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
| Method | Called when | Returns |
|---|---|---|
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: truewith thereferencethe core stores (the invite link for abearer_link, your handle for amembership,guild:role:userfor arole),grantedAt, andheld: false.granted: true, held: truefor a dated request your manifest says you may pre-issue: the member can present the reference and wait at the door untiladmit().granted: falsewith the classifiedfailure.UnreachableandTargetMissingare recorded on the grant as failed;NotPermittedandConfigurationbecome 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?
Messenger
Sending the core's messages through a connector — send, edit, delete and sendFile, the Message and Recipient objects, and everything the core checks before your port is called.
SupportRelay
Carrying a support conversation between a member and a creator over the platform — relay modes, replies, group threads and attachments, and where the inbox stays.