@vantageos/cloud-identity 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,78 @@
3
3
  All notable changes to `@vantageos/cloud-identity` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.9.0]
7
+
8
+ **Additive — the person principal (`./person-principal`).** The decisions a
9
+ server needs to serve a signed-in person in its own name, extracted from the
10
+ first consumer so that every other one wires them the same way.
11
+
12
+ ### Added
13
+
14
+ - `resolvePersonPrincipal`: a presented token's record -> `{ subject, orgSlug,
15
+ orgRole?, actor }`, or a typed refusal (not live, not a person's, no
16
+ organisation, organisation inactive or unmapped). Only the token's own
17
+ organisation is ever looked up.
18
+ - `resolvePersonActingName` / `checkPersonCallShape`: the own-name rule. The
19
+ acting identity comes from the principal; another user's name and an agent's
20
+ name without that agent's credential are refused.
21
+ - `resolvePersonTenantAccess`: strict organisation equality; unstamped refused.
22
+ - `resolveWriterRole` / `requireWriterRole`: writer-role assertion, fail closed.
23
+ - `PERSON_ACTOR_PREFIX`, `personActorName`, `isPersonActorName`,
24
+ `personTokenRecordSchema`, `PersonRefusalError` and the refusal types.
25
+ - New subpath export `./person-principal`.
26
+
27
+ ## [0.8.0]
28
+
29
+ **Additive — authorize a PERSON and bind an organisation (0.8.0).**
30
+ A connector's `/authorize` that auto-approves with no user authentication, and
31
+ derives the code's user from a client-registration profile, makes every client
32
+ an anonymous app. This release is the flow that replaces it: Clerk session,
33
+ verified organisation membership, a code bound to both, and an exchange that
34
+ re-derives nothing from the client's profile.
35
+
36
+ ### Added
37
+
38
+ - `startAuthorize` / `resumeAuthorize` (`src/authorize-flow.ts`): request
39
+ validation (exact `redirect_uri`, PKCE challenge required, allowed `resource`,
40
+ supported scopes); no Clerk session -> redirect to sign-in with an
41
+ HMAC-signed, short-lived state blob and no code; verified session -> one
42
+ organisation auto-picked, several -> an `OrgPickerModel`, a posted `orgId`
43
+ honoured only if it is in the user's own membership list; `requireConsent`
44
+ (default ON; opt out explicitly for first-party clients only). The code is single-use, short-lived and stored as a digest
45
+ through a consumer-supplied `AuthorizationCodeStore` whose `consume` must be
46
+ atomic.
47
+ - `exchangeAuthorizationCode` (`src/token-exchange.ts`): consumes the code
48
+ first, then checks expiry, client, `redirect_uri`, `resource` and the PKCE
49
+ verifier; returns `{ sub, org_id, org_slug, org_role, aud, client_id, scope }`.
50
+ - `verifyClerkSessionToken` (`src/clerk-session.ts`): RS256 against a
51
+ consumer-supplied key set; issuer, expiry, `nbf`, optional audience and
52
+ authorized parties.
53
+ - `buildDiscoveryDocument`, `buildUserInfo` (`src/oidc.ts`). The document
54
+ advertises only what is implemented (no id_token alg, no refresh grant, no
55
+ client-auth methods).
56
+ - `AuthorizeRefusal` / `AuthorizeRefusalReason` (`{ code: "AUTHORIZE_REFUSED",
57
+ reason }`), `oauthErrorFor`, `pkceChallengeFromVerifier`, and the types listed
58
+ in the README section "Authorize a person and bind an organisation".
59
+ - Subpath exports `/authorize`, `/token-exchange`, `/clerk-session`, `/oidc`.
60
+
61
+ ### Security
62
+
63
+ - Consent is bound to a server-issued `consentToken` (HMAC over state, user and
64
+ organisation set). `approved: true` without a token valid for that state and
65
+ that session's user is refused with `consent-required`; a client can no
66
+ longer mint its own state and approve on a victim's behalf.
67
+
68
+ ### Changed
69
+
70
+ - README: the "does not verify a signed JWT" limit now names the one exception.
71
+ - `package.json` version set to 0.8.0 (proposed; not published).
72
+
73
+ ### Unchanged
74
+
75
+ - Every existing export keeps its behaviour and signature; no existing test
76
+ was edited. No new runtime dependency (Web Crypto only).
77
+
6
78
  ## [0.7.0]
7
79
 
8
80
  **Additive — resolution of a PRESENTED, non-master bearer.** The published
package/README.md CHANGED
@@ -186,6 +186,15 @@ Visibility is granted two ways: the row's `createdBy` appears in the caller's
186
186
  `namespaceReadPrefixes`. Prefixes match on a path boundary, so a caller allowed
187
187
  `team/finance` does not thereby see `team/finance-archive`.
188
188
 
189
+ A third way is opt-in per call. All three functions take an optional last
190
+ argument, `grantFields` (type `GrantFieldDeclaration`, a list of property
191
+ names). It names the row properties that carry per-row grants, such as a
192
+ mission's `agents`. A row is then also visible when one of those properties
193
+ names an identity in the caller's `fromAllowList`. The property can hold a
194
+ single string or an array of strings; a value of any other shape is skipped,
195
+ not matched. The list is data supplied by your call site; the package
196
+ hardcodes no field names. Omitting it, or passing `[]`, adds nothing.
197
+
189
198
  **Token validation**
190
199
 
191
200
  - `validateMasterBearer(header, secret)` — compares an `Authorization: Bearer`
@@ -213,6 +222,8 @@ want to catch by type, and `BearerPayload`, the type returned by
213
222
  - `ValidateMasterBearerResult` — the return type of `validateMasterBearer`.
214
223
  - `ScopeFilterable` — the minimal row shape (`{ createdBy?, namespace? }`)
215
224
  accepted by `passesScopeFilter` / `scopeFilterList` / `scopeFilterGet`.
225
+ - `GrantFieldDeclaration` — `readonly string[]`, the optional `grantFields`
226
+ argument of those three functions.
216
227
  - `SessionIdentity` — the human-path shape `requireTenantId` accepts under
217
228
  `{ kind: "session", identity }` (see "Both caller paths, one contract").
218
229
  - `TenantSource` — the discriminated union `requireTenantId` accepts:
@@ -275,6 +286,170 @@ want to catch by type, and `BearerPayload`, the type returned by
275
286
  // -> { tenant: "org_abc", subject: "user_123", role: "admin" }
276
287
  ```
277
288
 
289
+ ## Authorize a person and bind an organisation
290
+
291
+ A connector that signs in a PERSON (a Claude or ChatGPT user) must end up with
292
+ a token naming that person and one organisation they belong to, never an
293
+ anonymous app. Clerk is the identity provider; this package holds the
294
+ decisions and takes every collaborator as a parameter.
295
+
296
+ ```ts
297
+ // GET /authorize
298
+ const out = await startAuthorize(query, clerkSessionToken, cfg, deps);
299
+ // GET /authorize/callback (the return from Clerk) and the picker's POST
300
+ const out2 = await resumeAuthorize({ state, sessionToken, orgId, approved, consentToken }, cfg, deps);
301
+ // POST /token (authorization_code grant)
302
+ const res = await exchangeAuthorizationCode(
303
+ { code, codeVerifier, redirectUri, clientId, resource },
304
+ { codeStore },
305
+ );
306
+ ```
307
+
308
+ - `startAuthorize(params, sessionToken, config, deps)` validates the request
309
+ (registered client, exact `redirect_uri`, a PKCE challenge using the SHA-256
310
+ method, an allowed RFC 8707 `resource`, supported scopes). With no valid Clerk
311
+ session it returns `{ kind: "redirect-to-sign-in", url }` and issues nothing;
312
+ the return URL carries a state blob signed with HMAC (`AuthorizeConfig.stateSecret`,
313
+ at least 32 characters) and valid for 600 seconds by default.
314
+ - `resumeAuthorize(input, config, deps)` rebuilds the request ONLY from that
315
+ signed state (a tampered or expired blob is refused), verifies the Clerk
316
+ session token (`verifyClerkSessionToken`: RS256 against the key set the
317
+ consumer supplies, issuer, expiry, optional audience and authorized parties),
318
+ lists the user's organisations through `deps.listMemberships`, and answers
319
+ with an `AuthorizeOutcome`:
320
+ `redirect-to-sign-in`, `org-picker` (an `OrgPickerModel` the consumer renders;
321
+ it lists only the user's own organisations), `redirect-to-client` (the URL
322
+ carrying the code and the client's `state`) or `refused`.
323
+ A posted `orgId` is honoured only if the verified user's own membership list
324
+ contains it.
325
+ Consent is ON by default (`requireConsent` defaults to true). What it
326
+ protects: a code is issued only after the person was SHOWN the picker and
327
+ approved it. The picker's `OrgPickerModel` carries a `consentToken`:
328
+ `HMAC(stateSecret, "consent-v1" NUL state NUL clerkUserId NUL sorted
329
+ organisation ids)`. It is bound to the exact signed state (useless for any
330
+ other request), to the verified user (useless under another session) and to
331
+ the organisation set that user was shown. `resumeAuthorize` with
332
+ `approved: true` refuses with `consent-required` unless the presented
333
+ `consentToken` verifies, compared in constant time, for that state AND the
334
+ session's user. Without this, a client could mint its own state (the sign-in
335
+ redirect hands it one), replay it with a victim's session and
336
+ `approved: true`, and receive a code the victim never saw a screen for.
337
+ The consumer renders `consentToken` as a hidden field of the picker's POST
338
+ form and posts it back as `consentToken`. The token IS the CSRF protection
339
+ for that POST, because a cross-site form cannot know it; do not add a weaker
340
+ substitute and do not accept the POST without it. Also set
341
+ `session.authorizedParties` to your own origin(s) so a Clerk session token
342
+ minted for another application (`azp`) is not accepted here.
343
+ Set `requireConsent: false` explicitly ONLY for first-party clients whose
344
+ redirect URIs you control; exactly one organisation is then auto-picked and
345
+ no token is involved.
346
+ - The code is bound to `{ clerkUserId, orgId, orgSlug, orgRole, clientId,
347
+ redirectUri, codeChallenge, resource }`, stored as a digest through the
348
+ consumer's `AuthorizationCodeStore` (`put`, and an ATOMIC `consume`), single
349
+ use, 60 seconds by default. Records are `AuthorizationCodeRecord`; a consume
350
+ answers `ConsumeCodeResult`. `AuthorizeClient`, `ClerkOrgMembership`,
351
+ `AuthorizeDeps`, `ResumeInput` and `ClerkJwk` / `ClerkJwks` /
352
+ `VerifiedClerkSession` / `VerifyClerkSessionResult` /
353
+ `ClerkSessionVerifierConfig` are the shapes the consumer supplies or reads.
354
+ - `exchangeAuthorizationCode(input, deps)` consumes the code FIRST (every
355
+ attempt burns it), then checks expiry, client, `redirect_uri`, resource and
356
+ the PKCE verifier (`pkceChallengeFromVerifier` derives a challenge). It
357
+ returns `AuthorizedTokenClaims`:
358
+ `{ sub, org_id, org_slug, org_role, aud, client_id, scope }` (`ExchangeInput`,
359
+ `ExchangeDeps`, `ExchangeResult`). No field comes from a client-registration
360
+ profile.
361
+ - `buildDiscoveryDocument(config)` builds the discovery document
362
+ (`DiscoveryConfig`, `DiscoveryDocument`, `DiscoveryResult`); the issuer must
363
+ be https with no query. It advertises only what is implemented: the code
364
+ flow, PKCE, the `authorization_code` grant and UserInfo. It omits
365
+ `id_token_signing_alg_values_supported` (OIDC Discovery lists it as required
366
+ for a provider that issues id_tokens; this package issues none, so that is a
367
+ documented deviation), refresh tokens and client-authentication methods. A
368
+ consumer that implements any of them adds those keys itself. `buildUserInfo(claims, user)` returns `sub`, and
369
+ `email` / `email_verified` when the token carries `email`
370
+ (`ClerkUserLike`, `UserInfo`, `UserInfoResult`).
371
+ - Every refusal is `{ code: "AUTHORIZE_REFUSED", reason }`
372
+ (`AuthorizeRefusal`, `AuthorizeRefusalReason`), the same shape as
373
+ `RoleRefusal`. `oauthErrorFor(refusal)` maps it to an OAuth error code and an
374
+ HTTP status (503 for a collaborator that failed, so an outage is not reported
375
+ as a bad credential). A collaborator that throws is always a refusal.
376
+
377
+ Also exported as subpaths: `/authorize`, `/token-exchange`, `/clerk-session`,
378
+ `/oidc`.
379
+
380
+ Limits, stated: `redirect_uri` matching is exact (no loopback port variation);
381
+ the package signs no access token and no id token and issues no refresh token (the consumer mints the token
382
+ from the claims); it fetches no key set and calls no Clerk API itself.
383
+
384
+ ## Wiring person sign-in in an MCP server
385
+
386
+ A person signed in through an OAuth connector reaches your MCP server holding a
387
+ bearer you minted for them (see "Authorize a person and bind an organisation").
388
+ `@vantageos/cloud-identity/person-principal` turns that bearer's token record
389
+ into the person's principal and answers the three questions every write needs.
390
+ It is pure: you do the lookups, it decides. A server that serves people (a PDF
391
+ inspector, a registry, a CRM) wires it the same way:
392
+
393
+ ```ts
394
+ import {
395
+ resolvePersonPrincipal,
396
+ resolvePersonActingName,
397
+ resolvePersonTenantAccess,
398
+ resolveWriterRole,
399
+ type PersonPrincipal,
400
+ type PersonRefusal,
401
+ } from "@vantageos/cloud-identity/person-principal";
402
+
403
+ // 1. Who is calling: the token row you looked up by the bearer's digest.
404
+ const who = await resolvePersonPrincipal(
405
+ { principal: row.principal, subject: row.userId, orgSlug: row.orgSlug,
406
+ orgRole: row.orgRole, revokedAt: row.revokedAt, expiresAt: row.expiresAt },
407
+ { now: Date.now(), lookupOrganisation: (slug) => orgTable.get(slug) },
408
+ "my-tool",
409
+ );
410
+ if (!who.ok) return deny(who.refusal);
411
+
412
+ // 2. Under what name: the token's, never the argument's.
413
+ const name = resolvePersonActingName({
414
+ principal: who.principal, claimedName: args.createdBy,
415
+ agentCredential: presentedAgent, door: "my-tool",
416
+ });
417
+ if (!name.ok) return deny(name.refusal);
418
+
419
+ // 3. Which rows, and may they write.
420
+ const inOrg = resolvePersonTenantAccess({
421
+ principal: who.principal, rowOrgId: record.orgId, door: "my-tool" });
422
+ const mayWrite = resolveWriterRole({
423
+ role: who.principal.orgRole, writerRoles: writerRolesOfOrg, door: "my-tool",
424
+ orgSlug: who.principal.orgSlug });
425
+ ```
426
+
427
+ - `resolvePersonPrincipal(token, deps, door)` refuses a token that is expired,
428
+ revoked, not a person's, bound to no organisation, or bound to an inactive or
429
+ unmapped one. It looks up only the token's own organisation.
430
+ - `resolvePersonActingName` returns `{ ok: true, actingAs: "person", actor }`
431
+ for no name or the person's own (`user:<subject>`), and refuses another
432
+ user's name (`PERSON_ACTS_AS_ITSELF`) and an agent's name without that
433
+ agent's own credential (`AGENT_CREDENTIAL_REQUIRED`, or
434
+ `AGENT_IDENTITY_MISMATCH` when the credential belongs to another agent).
435
+ `checkPersonCallShape` is the same rule for a backend door that receives a
436
+ person's call already stripped of its name. `PERSON_ACTOR_PREFIX`,
437
+ `personActorName` and `isPersonActorName` spell and reserve the actor name:
438
+ refuse it when someone registers an agent.
439
+ - `resolvePersonTenantAccess` is a strict equality on the organisation; an
440
+ unstamped row is refused.
441
+ - `resolveWriterRole` / `requireWriterRole` check the verified role against a
442
+ writer allowlist you hold as data. A missing role, a missing list and an
443
+ empty list all refuse. `requireWriterRole` throws `PersonRefusalError`.
444
+ - Every refusal is a `PersonRefusal` (`PersonRefusalCode`, `PersonRefusalReason`,
445
+ `door`, `detail`); you choose the transport shape. `personTokenRecordSchema`,
446
+ `PersonTokenRecord`, `PersonPrincipal`, `OrganisationState`,
447
+ `PersonPrincipalDeps`, `PersonPrincipalResult`, `PersonActingNameResult` and
448
+ `PersonAccessResult` are the input and result types.
449
+
450
+ The module verifies no credential. The token record must come from your own
451
+ lookup of the bearer the person presented, never from a request body.
452
+
278
453
  ## What this package does not do
279
454
 
280
455
  Worth reading before you rely on it.
@@ -296,10 +471,11 @@ Worth reading before you rely on it.
296
471
  a token, writing its row, marking it revoked and expiring it are yours; the
297
472
  package only insists that a row it is handed be believed in constant time,
298
473
  and refused identically however it fails.
299
- - **It does not verify a signed JWT.** The presented-bearer path is for an
300
- OPAQUE token — one whose only meaning is the row it matches. If your
301
- credential carries its own signed claims, verify that signature with a JWT
302
- library first; this package exports no verifier.
474
+ - **It does not verify an arbitrary signed JWT.** The presented-bearer path is
475
+ for an OPAQUE token — one whose only meaning is the row it matches. The one
476
+ signed token it verifies is a Clerk session token (`verifyClerkSessionToken`,
477
+ RS256 only). For any other signed credential, verify the signature with a
478
+ JWT library first.
303
479
 
304
480
  ## Upgrading to 0.4.0
305
481
 
@@ -0,0 +1,169 @@
1
+ import { type AuthorizeRefusal } from "./authorize-shared.js";
2
+ import { type ClerkSessionVerifierConfig } from "./clerk-session.js";
3
+ /**
4
+ * The authorize flow for a PERSON: a Clerk session, a verified organisation
5
+ * membership, and an authorization code bound to both. Framework-agnostic:
6
+ * every function takes plain values and returns a plain outcome; the consumer
7
+ * owns HTTP, rendering and storage.
8
+ *
9
+ * The code is bound to `{ clerkUserId, orgId, orgSlug, orgRole, clientId,
10
+ * redirectUri, codeChallenge, resource }`. Nothing is read from a
11
+ * client-registration profile: a client's own fields only decide whether the
12
+ * request is acceptable, never what the person may reach.
13
+ */
14
+ /** A registered OAuth client, as the consumer's own registry knows it. */
15
+ export interface AuthorizeClient {
16
+ clientId: string;
17
+ /** Exact-match list; no prefix, wildcard or partial match is ever applied. */
18
+ redirectUris: readonly string[];
19
+ clientName?: string;
20
+ revoked?: boolean;
21
+ }
22
+ /** One organisation membership, in the shape Clerk's Backend API returns. */
23
+ export interface ClerkOrgMembership {
24
+ role: string;
25
+ organization: {
26
+ id: string;
27
+ slug: string | null;
28
+ name: string;
29
+ };
30
+ }
31
+ /** A stored authorization code. Only the digest of the code is stored. */
32
+ export interface AuthorizationCodeRecord {
33
+ codeHash: string;
34
+ clerkUserId: string;
35
+ orgId: string;
36
+ orgSlug: string | null;
37
+ orgRole: string;
38
+ clientId: string;
39
+ redirectUri: string;
40
+ codeChallenge: string;
41
+ resource: string;
42
+ scope: string;
43
+ /** Epoch milliseconds. */
44
+ expiresAt: number;
45
+ }
46
+ export type ConsumeCodeResult = {
47
+ status: "ok";
48
+ record: AuthorizationCodeRecord;
49
+ } | {
50
+ status: "unknown";
51
+ } | {
52
+ status: "already-used";
53
+ };
54
+ /**
55
+ * Where codes live. `consume` MUST be atomic: of two concurrent calls for the
56
+ * same digest exactly one gets `ok`. A read-then-write is not an
57
+ * implementation of this interface.
58
+ */
59
+ export interface AuthorizationCodeStore {
60
+ put(record: AuthorizationCodeRecord): Promise<void>;
61
+ consume(codeHash: string): Promise<ConsumeCodeResult>;
62
+ }
63
+ export interface AuthorizeConfig {
64
+ /** HMAC secret for the state blob; at least 32 characters. Never sent to a client. */
65
+ stateSecret: string;
66
+ /** Clerk sign-in page. The return URL travels in `redirect_url`. */
67
+ signInUrl: string;
68
+ /** The consumer's endpoint that calls `resumeAuthorize`. */
69
+ callbackUrl: string;
70
+ /** Resource identifiers (RFC 8707) this server issues codes for. Exact match. */
71
+ allowedResources: readonly string[];
72
+ /** When set, every requested scope must be listed. */
73
+ supportedScopes?: readonly string[];
74
+ /** Scope granted when the request names none. Default `"openid"`. */
75
+ defaultScope?: string;
76
+ /** Verification of the Clerk session token. */
77
+ session: ClerkSessionVerifierConfig;
78
+ /** Seconds the signed state stays valid. Default 600. */
79
+ stateTtlSeconds?: number;
80
+ /** Seconds an authorization code stays valid. Default 60. */
81
+ codeTtlSeconds?: number;
82
+ /**
83
+ * The person must explicitly approve (the outcome is a consent model, not a
84
+ * code, until `approved: true` is posted back). DEFAULT TRUE: without it, any
85
+ * dynamically registered client could obtain a code for a signed-in victim
86
+ * who merely follows a link. Set `false` explicitly only for first-party
87
+ * clients whose redirect URIs you control; a single organisation is then
88
+ * auto-picked.
89
+ */
90
+ requireConsent?: boolean;
91
+ }
92
+ export interface AuthorizeDeps {
93
+ lookupClient(clientId: string): Promise<AuthorizeClient | null>;
94
+ /** Clerk Backend API: the user's organisation memberships. */
95
+ listMemberships(clerkUserId: string): Promise<readonly ClerkOrgMembership[]>;
96
+ codeStore: AuthorizationCodeStore;
97
+ /** Epoch milliseconds. */
98
+ now?: () => number;
99
+ /** Opaque code generator; default 32 random bytes, base64url. */
100
+ generateCode?: () => string;
101
+ }
102
+ export interface OrgPickerModel {
103
+ /** Signed state; post it back, untouched, to `resumeAuthorize`. */
104
+ state: string;
105
+ client: {
106
+ clientId: string;
107
+ clientName: string | null;
108
+ };
109
+ resource: string;
110
+ scope: string;
111
+ /** Only organisations the verified user is a member of. */
112
+ organizations: readonly {
113
+ id: string;
114
+ slug: string | null;
115
+ name: string;
116
+ role: string;
117
+ }[];
118
+ /** True when the person must tick approval even with one organisation. */
119
+ consentRequired: boolean;
120
+ /**
121
+ * Proof that THIS picker was shown to THIS verified user for THIS state.
122
+ * Render it as a hidden field and post it back as `consentToken`. Absent
123
+ * when consent is not required.
124
+ */
125
+ consentToken: string | null;
126
+ }
127
+ export type AuthorizeOutcome = {
128
+ kind: "redirect-to-sign-in";
129
+ url: string;
130
+ } | {
131
+ kind: "org-picker";
132
+ model: OrgPickerModel;
133
+ } | {
134
+ kind: "redirect-to-client";
135
+ url: string;
136
+ } | {
137
+ kind: "refused";
138
+ refusal: AuthorizeRefusal;
139
+ };
140
+ export interface ResumeInput {
141
+ /** The `authorize_state` query parameter on the return URL (or the picker's posted state). */
142
+ state: string;
143
+ /** The Clerk session token presented on return. */
144
+ sessionToken: string | undefined;
145
+ /** The organisation the person chose in the picker. */
146
+ orgId?: string;
147
+ /** The person's explicit approval, when `requireConsent` is on. */
148
+ approved?: boolean;
149
+ /** The `consentToken` of the picker the person was shown. Required with `approved: true` when consent is on. */
150
+ consentToken?: string;
151
+ }
152
+ /**
153
+ * Handles `GET /authorize`. Validates the request against the registered
154
+ * client, then:
155
+ * - no Clerk session (absent, expired or forged) -> `redirect-to-sign-in`,
156
+ * carrying a signed, short-lived state blob and issuing NO code;
157
+ * - a verified session -> continues exactly as `resumeAuthorize` does.
158
+ * A Clerk key set that cannot be reached is a refusal (`jwks-unavailable`),
159
+ * never a redirect and never a grant.
160
+ */
161
+ export declare function startAuthorize(params: Readonly<Record<string, string | undefined>>, sessionToken: string | undefined, cfg: AuthorizeConfig, deps: AuthorizeDeps): Promise<AuthorizeOutcome>;
162
+ /**
163
+ * Handles the return from Clerk (and the picker's form post). The request is
164
+ * rebuilt ONLY from the signed state: no query parameter other than the blob
165
+ * itself is read. A tampered or expired blob is refused; the client is looked
166
+ * up again so a client revoked mid-flow gets no code.
167
+ */
168
+ export declare function resumeAuthorize(input: ResumeInput, cfg: AuthorizeConfig, deps: AuthorizeDeps): Promise<AuthorizeOutcome>;
169
+ //# sourceMappingURL=authorize-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize-flow.d.ts","sourceRoot":"","sources":["../src/authorize-flow.ts"],"names":[],"mappings":"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"}