AccessController

Giving and taking away access to a place — grant, revoke, revokeReference, admit, membership, announce and reconcile, the idempotency each must keep, and every core path that calls them.

Subscriby\Connector\Contracts\Ports\AccessController. Bound by access_control; early_admission_hold adds admit() on the same class.

The core keeps the access ledger (who should be where, per subscription, resource and window); the connector holds the platform's side of it: the invite link, the membership, the role. Every method is idempotent, because the core retries and reconciles: granting twice yields one grant, revoking what the platform already lost reports revoked.

Methods

MethodCalled whenReturns
grant(installation, credentials, GrantRequest)A purchase, renewal or access-code redemption is settled; a pending grant materialises when an identity is linked; Refresh access (a reissue); a member is re-admitted after a place is replaced.GrantResult
revoke(installation, credentials, GrantRef, SpaceRef, IdentityRef)A subscription is cancelled, expires, is paused; a member is banned or kicked; a connector is uninstalled.RevokeResult
revokeReference(installation, credentials, GrantRef, SpaceRef)A grant's reference must die while its holder keeps the place: another grant still covers them, the holder owns the place, or a reissue replaces the reference.RevokeResult
admit(installation, credentials, GrantRef, SpaceRef, IdentityRef)A pass window opens and the ledger holds a held grant for it.GrantResult
membership(installation, credentials, SpaceRef, IdentityRef)The core needs a member's standing in a place right now: before a removal, on the portal's membership card.Membership
announce(installation, credentials, IdentityRef, GrantAnnouncement)A run of grants has finished (a sale settled, a code redeemed, a presale opened, a reissue, a creator task completed) and the holder should be told what they hold.void
reconcile(installation, credentials, iterable<GrantSnapshot>)Every fifteen minutes, per linked resource, with the grants revoked in the last day and the live grants whose turn it is: each live grant is checked once a day in a fixed quarter-hour slot, on every run for a project whose owner's plan carries Disaster Recovery prevention, and every live grant after a connector outage ends.ReconcileReport

grant()

new GrantRequest(
    space: $spaceRef,          // the place
    identity: $identityRef,    // the account that gets in
    mode: GrantMode::BearerLink,
    existing: $grantRef,       // the grant being re-asserted or reissued, or null
    opensAt: null,             // a date for a pass whose window has not opened
    meta: [],
);

Give the account access in the mode the resource kind declares and answer a GrantResult:

  • granted: true with the reference the core stores (the invite link for a bearer_link, your handle for a membership, guild:role:user for a role), grantedAt, and held: false.
  • granted: true, held: true for a dated request your manifest says you may pre-issue: the member can present the reference and wait at the door until admit().
  • granted: false with the classified failure. Unreachable and TargetMissing are recorded on the grant as failed; NotPermitted and Configuration become a health verdict the creator is shown.

existing names the grant when the core is re-asserting or reissuing; a connector whose reference is stable (a role) answers with the same reference, one whose reference is consumable (a single-use link) mints a fresh one.

revoke() and revokeReference()

revoke() takes the account out of the place and kills whatever the grant issued. Answer revoked: true even when the platform reports the account already gone: the ledger's state is what matters, and a missing grant is not an error.

revokeReference() withdraws only what the grant issued while the holder stays: another live grant still covers them, or they own the place. A connector whose grants carry no reference apart from the membership itself answers revoked without a platform call.

A removal for a dated reason (an expiry) is preceded by membership(), so the core can record attendance before the account is put out.

admit()

Let the holder of a held grant in now that its window has opened and answer GrantResult with the grant's reference. Throw UnsupportedByConnector when the manifest declares no early_admission_hold; the core never asks a connector without it, because such a connector was never allowed to hold.

announce()

How a holder is shown what they hold is the connector's idiom: a list of links to tap, a sentence saying a role appeared, nothing at all when the platform shows it. GrantAnnouncement carries the grants this run issued and the windowId it was about; you may list everything the holder holds rather than only what the announcement names, because a holder of several purchases expects every link in one place. Send through your own Messenger (the core has already picked the installation to reach the holder through) and return nothing; a connector whose grants need no telling returns at once.

reconcile()

Bring the platform into line with the ledger for one installation: each GrantSnapshot names a grant, its space, its identity and its state; let banned holders of live grants back in, put holders of dead grants out, leave the rest alone, and answer a ReconcileReport (checked, reasserted, revoked, failures). The sweep is what repairs a platform that was unreachable when the grant happened or an event the platform never delivered, so make it assert a state rather than replay events.

The membership() answer

Membership carries a MembershipStatus (Owner, Administrator, Member, Restricted, Left, Banned, Unknown) and since. Answer Unknown when the platform cannot say, never a guess.

What the core does around you

Every call goes through the core's ConnectorAccessController, which resolves the resource's place (installation, credentials, SpaceRef), the member's identity on that connector and the grant's ref, times and counts the call, and reads your result. A member with no identity on the connector is never asked of you: the grant waits as pending_identity and materialises when the identity is linked.

How Telegram does it

Telegram cannot add anyone to anything, so a grant is a personal invite link that creates a join request the bot approves, and the link is the grant's reference; a dated grant is pre-issued and reported held, because the join request waits at the door until the window opens. Taking access away revokes the link and then bans and unbans the member, which puts them outside while leaving them free to be invited back; withdrawing the reference only revokes the link. announce() sends the member their links; reconcile() compares getChatMember with the ledger.

What the kit checks

access.declares_kinds: a connector that binds the port lists at least one resource kind. Behaviour is exercised by Subscriby's suite through the fake connector's FakeAccessController (assertGranted(), assertNotGranted(), assertAnnounced(), assertNotAnnounced()), and by your own tests against your platform's fake.

Never decide entitlement

Whether an account should be in a place is the core's decision, made by its access policy from the ledger. Your port only does what it is told and reports what happened; a connector that checks a subscription before granting will disagree with the ledger the first time a policy changes.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request