@aooth/auth-moost 0.1.16 → 0.1.18

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.
@@ -1,2 +1,2 @@
1
- import { C as SetPasswordForm, E as WithInlineConsentForm, S as Select2faForm, T as TermsBumpForm, _ as ProveControlForm, a as EmailIdentifierForm, b as RecoveryModeSelectForm, c as EnrollPickMethodForm, d as LoginCredentialsForm, f as MagicLinkRequestForm, g as PincodeForm, h as PasswordReauthForm, i as ConcurrencyLimitForm, l as EnrollTotpQrForm, m as MfaCodeForm, n as AskPhoneForm, o as EnrollAddressForm, p as ManageMfaForm, r as ChangePasswordForm, s as EnrollConfirmForm, t as AskEmailForm, u as InviteForm, v as ProveControlOtpForm, w as SignupForm, x as RemoveMfaConfirmForm, y as RecoveryFactorForm } from "../forms-11EDZgS1.mjs";
2
- export { AskEmailForm, AskPhoneForm, ChangePasswordForm, ConcurrencyLimitForm, EmailIdentifierForm, EnrollAddressForm, EnrollConfirmForm, EnrollPickMethodForm, EnrollTotpQrForm, InviteForm, LoginCredentialsForm, MagicLinkRequestForm, ManageMfaForm, MfaCodeForm, PasswordReauthForm, PincodeForm, ProveControlForm, ProveControlOtpForm, RecoveryFactorForm, RecoveryModeSelectForm, RemoveMfaConfirmForm, Select2faForm, SetPasswordForm, SignupForm, TermsBumpForm, WithInlineConsentForm };
1
+ import { C as Select2faForm, D as WithInlineConsentForm, E as TermsBumpForm, S as RemoveMfaConfirmForm, T as SignupForm, _ as PincodeForm, a as ConcurrencyLimitForm, b as RecoveryFactorForm, c as EnrollConfirmForm, d as InviteForm, f as LoginCredentialsForm, g as PasswordReauthForm, h as MfaCodeForm, i as ChangePasswordForm, l as EnrollPickMethodForm, m as ManageMfaForm, n as AskPhoneForm, o as EmailIdentifierForm, p as MagicLinkRequestForm, r as AuthorizeConsentForm, s as EnrollAddressForm, t as AskEmailForm, u as EnrollTotpQrForm, v as ProveControlForm, w as SetPasswordForm, x as RecoveryModeSelectForm, y as ProveControlOtpForm } from "../forms-D7ZfanKT.mjs";
2
+ export { AskEmailForm, AskPhoneForm, AuthorizeConsentForm, ChangePasswordForm, ConcurrencyLimitForm, EmailIdentifierForm, EnrollAddressForm, EnrollConfirmForm, EnrollPickMethodForm, EnrollTotpQrForm, InviteForm, LoginCredentialsForm, MagicLinkRequestForm, ManageMfaForm, MfaCodeForm, PasswordReauthForm, PincodeForm, ProveControlForm, ProveControlOtpForm, RecoveryFactorForm, RecoveryModeSelectForm, RemoveMfaConfirmForm, Select2faForm, SetPasswordForm, SignupForm, TermsBumpForm, WithInlineConsentForm };
@@ -1,2 +1,2 @@
1
- import { C as SetPasswordForm, E as WithInlineConsentForm, S as Select2faForm, T as TermsBumpForm, _ as ProveControlForm, a as EmailIdentifierForm, b as RecoveryModeSelectForm, c as EnrollPickMethodForm, d as LoginCredentialsForm, f as MagicLinkRequestForm, g as PincodeForm, h as PasswordReauthForm, i as ConcurrencyLimitForm, l as EnrollTotpQrForm, m as MfaCodeForm, n as AskPhoneForm, o as EnrollAddressForm, p as ManageMfaForm, r as ChangePasswordForm, s as EnrollConfirmForm, t as AskEmailForm, u as InviteForm, v as ProveControlOtpForm, w as SignupForm, x as RemoveMfaConfirmForm, y as RecoveryFactorForm } from "../forms-11EDZgS1.mjs";
2
- export { AskEmailForm, AskPhoneForm, ChangePasswordForm, ConcurrencyLimitForm, EmailIdentifierForm, EnrollAddressForm, EnrollConfirmForm, EnrollPickMethodForm, EnrollTotpQrForm, InviteForm, LoginCredentialsForm, MagicLinkRequestForm, ManageMfaForm, MfaCodeForm, PasswordReauthForm, PincodeForm, ProveControlForm, ProveControlOtpForm, RecoveryFactorForm, RecoveryModeSelectForm, RemoveMfaConfirmForm, Select2faForm, SetPasswordForm, SignupForm, TermsBumpForm, WithInlineConsentForm };
1
+ import { C as Select2faForm, D as WithInlineConsentForm, E as TermsBumpForm, S as RemoveMfaConfirmForm, T as SignupForm, _ as PincodeForm, a as ConcurrencyLimitForm, b as RecoveryFactorForm, c as EnrollConfirmForm, d as InviteForm, f as LoginCredentialsForm, g as PasswordReauthForm, h as MfaCodeForm, i as ChangePasswordForm, l as EnrollPickMethodForm, m as ManageMfaForm, n as AskPhoneForm, o as EmailIdentifierForm, p as MagicLinkRequestForm, r as AuthorizeConsentForm, s as EnrollAddressForm, t as AskEmailForm, u as EnrollTotpQrForm, v as ProveControlForm, w as SetPasswordForm, x as RecoveryModeSelectForm, y as ProveControlOtpForm } from "../forms-D7ZfanKT.mjs";
2
+ export { AskEmailForm, AskPhoneForm, AuthorizeConsentForm, ChangePasswordForm, ConcurrencyLimitForm, EmailIdentifierForm, EnrollAddressForm, EnrollConfirmForm, EnrollPickMethodForm, EnrollTotpQrForm, InviteForm, LoginCredentialsForm, MagicLinkRequestForm, ManageMfaForm, MfaCodeForm, PasswordReauthForm, PincodeForm, ProveControlForm, ProveControlOtpForm, RecoveryFactorForm, RecoveryModeSelectForm, RemoveMfaConfirmForm, Select2faForm, SetPasswordForm, SignupForm, TermsBumpForm, WithInlineConsentForm };
@@ -234,6 +234,15 @@ var ProveControlOtpForm = class {
234
234
  throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
235
235
  }
236
236
  };
237
+ var AuthorizeConsentForm = class {
238
+ static __is_atscript_annotated_type = true;
239
+ static type = {};
240
+ static metadata = /* @__PURE__ */ new Map();
241
+ static id = "AuthorizeConsentForm";
242
+ static toJsonSchema() {
243
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
244
+ }
245
+ };
237
246
  defineAnnotatedType("object", WithInlineConsentForm).prop("consents", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("meta.label", "Pending consents").annotate("ui.form.component", "AsConsentArray").annotate("ui.form.fn.attr", {
238
247
  name: "pendingConsents",
239
248
  fn: "(_, _d, ctx) => ctx.public?.consents?.pending"
@@ -304,7 +313,7 @@ defineAnnotatedType("object", SetPasswordForm).prop("consents", defineAnnotatedT
304
313
  }, true).annotate("ui.form.fn.attr", {
305
314
  name: "password",
306
315
  fn: "(_, data) => data.newPassword"
307
- }, true).annotate("ui.form.grid.colSpan", { desktop: "12" }).$type).annotate("ui.form.fn.title", "(_, _d, ctx) => ctx.public?.password?.heading || \"Set your password\"").annotate("wf.context.pass", "public", true);
316
+ }, true).annotate("ui.form.grid.colSpan", { desktop: "12" }).$type).annotate("ui.form.fn.title", "(_, ctx) => ctx.public?.password?.heading || \"Set your password\"").annotate("wf.context.pass", "public", true);
308
317
  defineAnnotatedType("object", InviteForm).prop("email", defineAnnotatedType().designType("string").tags("email", "string").annotate("ui.form.order", 10).annotate("ui.form.type", "text").annotate("meta.label", "Email").annotate("ui.form.autocomplete", "email").annotate("meta.required", {}).annotate("expect.pattern", {
309
318
  pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
310
319
  flags: "",
@@ -355,7 +364,7 @@ defineAnnotatedType("object", EnrollAddressForm).prop("address", defineAnnotated
355
364
  }).annotate("ui.form.fn.hidden", "() => true").optional().$type).prop("useDifferentMethod", defineAnnotatedType().designType("phantom").tags("action", "ui").annotate("ui.form.action", {
356
365
  id: "useDifferentMethod",
357
366
  label: "Use a different method"
358
- }).annotate("ui.form.fn.hidden", "(_, _d, ctx) => (ctx.public?.mfaEnroll?.availableTransports?.length ?? 0) < 2 || ctx.public?.mfaEnroll?.mode === \"manage\"").optional().$type).annotate("ui.form.fn.title", "(_, _d, ctx) => ctx.public?.mfaEnroll?.method === \"sms\" ? \"Add your phone number\" : \"Add your email\"").annotate("meta.description", "We will send you a one-time code to confirm.").annotate("wf.context.pass", "public", true).annotate("ui.form.submit.text", "Send code");
367
+ }).annotate("ui.form.fn.hidden", "(_, _d, ctx) => (ctx.public?.mfaEnroll?.availableTransports?.length ?? 0) < 2 || ctx.public?.mfaEnroll?.mode === \"manage\"").optional().$type).annotate("ui.form.fn.title", "(_, ctx) => ctx.public?.mfaEnroll?.method === \"sms\" ? \"Add your phone number\" : \"Add your email\"").annotate("meta.description", "We will send you a one-time code to confirm.").annotate("wf.context.pass", "public", true).annotate("ui.form.submit.text", "Send code");
359
368
  defineAnnotatedType("object", EnrollConfirmForm).prop("transportHint", defineAnnotatedType().designType("phantom").tags("paragraph", "ui").annotate("ui.form.order", 1).annotate("ui.form.fn.value", "(_, _d, ctx) => ctx.public?.mfaEnroll?.method === \"totp\" ? \"Enter the 6-digit code from your authenticator app.\" : ctx.public?.pincode?.sentTo ? \"Code sent to \" + ctx.public.pincode.sentTo + \". Enter it below to confirm.\" : \"Enter the code to confirm enrollment.\"").optional().$type).prop("code", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.order", 10).annotate("ui.form.type", "text").annotate("meta.label", "Code").annotate("ui.form.autocomplete", "one-time-code").annotate("meta.required", {}).annotate("expect.minLength", { length: 4 }).annotate("expect.maxLength", { length: 12 }).annotate("expect.pattern", { pattern: "^[0-9]+$" }, true).$type).prop("resend", defineAnnotatedType().designType("phantom").tags("action", "ui").annotate("ui.form.action", {
360
369
  id: "resend",
361
370
  label: "Resend code"
@@ -417,7 +426,7 @@ defineAnnotatedType("object", ChangePasswordForm).prop("intro", defineAnnotatedT
417
426
  }, true).annotate("ui.form.fn.attr", {
418
427
  name: "password",
419
428
  fn: "(_, data) => data.newPassword"
420
- }, true).annotate("ui.form.grid.colSpan", { desktop: "12" }).$type).annotate("ui.form.fn.title", "(_, _d, ctx) => ctx.public?.password?.heading || \"Change your password\"").annotate("ui.form.submit.text", "Change password").annotate("wf.context.pass", "public", true);
429
+ }, true).annotate("ui.form.grid.colSpan", { desktop: "12" }).$type).annotate("ui.form.fn.title", "(_, ctx) => ctx.public?.password?.heading || \"Change your password\"").annotate("ui.form.submit.text", "Change password").annotate("wf.context.pass", "public", true);
421
430
  defineAnnotatedType("object", ProveControlForm).prop("intro", defineAnnotatedType().designType("phantom").tags("paragraph", "ui").annotate("ui.form.order", 5).annotate("ui.form.fn.value", "(_, _d, ctx) => ctx.public?.proveControl?.hint ? \"An account for \" + ctx.public.proveControl.hint + \" already exists. Enter its password to link this sign-in method.\" : \"Enter your existing account password to link this sign-in method.\"").$type).prop("password", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.order", 10).annotate("ui.form.type", "password").annotate("meta.label", "Password").annotate("ui.form.autocomplete", "current-password").annotate("meta.sensitive", true).annotate("meta.required", {}).annotate("expect.minLength", { length: 1 }).$type).prop("cancel", defineAnnotatedType().designType("phantom").tags("action", "ui").annotate("ui.form.order", 20).annotate("ui.form.action", {
422
431
  id: "cancel",
423
432
  label: "Cancel"
@@ -438,5 +447,12 @@ defineAnnotatedType("object", ProveControlOtpForm).prop("intro", defineAnnotated
438
447
  name: "align",
439
448
  value: "center"
440
449
  }, true).annotate("ui.form.pushDown", true).optional().$type).annotate("meta.label", "Confirm your identity").annotate("wf.context.pass", "public", true).annotate("ui.form.submit.text", "Verify and link");
450
+ defineAnnotatedType("object", AuthorizeConsentForm).prop("notice", defineAnnotatedType().designType("phantom").tags("paragraph", "ui").annotate("ui.form.order", 1).annotate("ui.form.fn.value", "(_, _d, ctx) => { const a = ctx.public?.authz; const who = a?.clientName ? \"“\" + a.clientName + \"”\" : \"A local application\"; const sc = a?.scope ? \" It is requesting access to: \" + a.scope + \".\" : \"\"; return who + \" wants to sign in to your account.\" + sc + \" Authorize this only if you started it.\"; }").$type).prop("deny", defineAnnotatedType().designType("phantom").tags("action", "ui").annotate("ui.form.order", 10).annotate("ui.form.action", {
451
+ id: "deny",
452
+ label: "Deny"
453
+ }).annotate("ui.form.attr", {
454
+ name: "align",
455
+ value: "center"
456
+ }, true).annotate("ui.form.pushDown", true).optional().$type).annotate("meta.label", "Authorize access").annotate("wf.context.pass", "public", true).annotate("ui.form.submit.text", "Authorize");
441
457
  //#endregion
442
- export { SetPasswordForm as C, WithInlineConsentForm as E, Select2faForm as S, TermsBumpForm as T, ProveControlForm as _, EmailIdentifierForm as a, RecoveryModeSelectForm as b, EnrollPickMethodForm as c, LoginCredentialsForm as d, MagicLinkRequestForm as f, PincodeForm as g, PasswordReauthForm as h, ConcurrencyLimitForm as i, EnrollTotpQrForm as l, MfaCodeForm as m, AskPhoneForm as n, EnrollAddressForm as o, ManageMfaForm as p, ChangePasswordForm as r, EnrollConfirmForm as s, AskEmailForm as t, InviteForm as u, ProveControlOtpForm as v, SignupForm as w, RemoveMfaConfirmForm as x, RecoveryFactorForm as y };
458
+ export { Select2faForm as C, WithInlineConsentForm as D, TermsBumpForm as E, RemoveMfaConfirmForm as S, SignupForm as T, PincodeForm as _, ConcurrencyLimitForm as a, RecoveryFactorForm as b, EnrollConfirmForm as c, InviteForm as d, LoginCredentialsForm as f, PasswordReauthForm as g, MfaCodeForm as h, ChangePasswordForm as i, EnrollPickMethodForm as l, ManageMfaForm as m, AskPhoneForm as n, EmailIdentifierForm as o, MagicLinkRequestForm as p, AuthorizeConsentForm as r, EnrollAddressForm as s, AskEmailForm as t, EnrollTotpQrForm as u, ProveControlForm as v, SetPasswordForm as w, RecoveryModeSelectForm as x, ProveControlOtpForm as y };
package/dist/index.d.mts CHANGED
@@ -181,7 +181,7 @@ declare function authGuardInterceptor(opts?: AuthOptions): TInterceptorDef;
181
181
  */
182
182
  declare function AuthGuarded(opts?: AuthOptions): ClassDecorator & MethodDecorator;
183
183
  //#endregion
184
- //#region ../../node_modules/.pnpm/@wooksjs+event-wf@0.7.17_@prostojs+logger@0.4.3_@wooksjs+event-core@0.7.17_@wooksjs+eve_308375c6ad6313773ebd6e419ace01e4/node_modules/@wooksjs/event-wf/dist/index.d.ts
184
+ //#region ../../node_modules/.pnpm/@wooksjs+event-wf@0.7.19_@prostojs+logger@0.4.3_@wooksjs+event-core@0.7.19_@wooksjs+eve_bb34de328332c0c9e9ce8e94ed9c5cac/node_modules/@wooksjs/event-wf/dist/index.d.ts
185
185
  interface WfFinishedResponse {
186
186
  type: 'redirect' | 'data';
187
187
  /** Redirect URL or response body */
@@ -946,6 +946,39 @@ declare class AuthorizeRuntime {
946
946
  constructor(pending: PendingAuthorizationStore, codes: AuthCodeStore);
947
947
  }
948
948
  //#endregion
949
+ //#region src/authz/authz-binding.d.ts
950
+ /**
951
+ * Name of the double-submit cookie that binds an in-flight authorization
952
+ * request to the browser that started it. Set at `GET /auth/authorize` (holding
953
+ * the high-entropy `binding` secret recorded on the `PendingAuthorization`), and
954
+ * matched — constant-time, via `safeEqual` from `oauth-csrf` — at the
955
+ * `mint-authz-code` / `authz-consent` workflow steps before any code is minted.
956
+ *
957
+ * Why this closes the account-takeover (AUTH-SERVER.md §6): the opaque `authz`
958
+ * handle alone is a bearer ticket — anyone holding it can drive it to the
959
+ * code-minting terminal. An attacker who initiates `/auth/authorize` (to inject
960
+ * their own client / redirect / PKCE challenge) receives the handle AND this
961
+ * cookie in THEIR browser; phishing only the handle into a victim's browser
962
+ * fails the match here, because the victim's browser never holds the secret. So
963
+ * a handle can only be redeemed by the same browser that started the request.
964
+ */
965
+ declare const AUTHZ_BINDING_COOKIE = "aooth_authz";
966
+ /**
967
+ * Cookie attributes for the authorize-binding cookie — the same httpOnly /
968
+ * `SameSite=Lax` shape as the OAuth CSRF cookie (Lax, NOT Strict, so the
969
+ * top-level GET navigation BACK from a "Continue with <provider>" detour still
970
+ * carries it), so it delegates to {@link oauthCsrfCookieAttrs}. `maxAgeSec` is
971
+ * the pending authorization's remaining lifetime — the caller derives it from
972
+ * the row's `expiresAt` (returned by `PendingAuthorizationStore.create`), so the
973
+ * cookie tracks the row's TTL even when a store is configured with a non-default
974
+ * `ttlMs`. `secure` is caller-controlled (off for the http test harness, on in
975
+ * production).
976
+ */
977
+ declare function authzBindingCookieAttrs(opts: {
978
+ secure: boolean;
979
+ maxAgeSec: number;
980
+ }): TCookieAttributesInput;
981
+ //#endregion
949
982
  //#region src/authz/authz-tokens.d.ts
950
983
  /**
951
984
  * Explicit string DI tokens for the ABSTRACT authorization-server stores
@@ -1542,6 +1575,16 @@ interface AuthWfPublicState {
1542
1575
  * verify forms when a forced password change will follow.
1543
1576
  */
1544
1577
  newPasswordRequired?: boolean;
1578
+ /**
1579
+ * Mirrors the display-only fields of `ctx.authz` — the requesting client's
1580
+ * id/name and granted scope, shown on the authorize-consent form. The
1581
+ * `handle` and the `approved` gate stay server-only (never whitelisted onto
1582
+ * the wire).
1583
+ */
1584
+ authz?: {
1585
+ clientName?: string;
1586
+ scope?: string;
1587
+ };
1545
1588
  }
1546
1589
  /** Unified workflow context shape — one type for all three flows. */
1547
1590
  interface AuthWfCtx {
@@ -1612,11 +1655,17 @@ interface AuthWfCtx {
1612
1655
  * input `authz` (the opaque pending-authorization handle), and `sso-callback`
1613
1656
  * re-raises it from the federated `state.handle` when the user took a
1614
1657
  * "Continue with <provider>" detour mid-authorize. Presence routes the login
1615
- * tail to the `mint-authz-code` terminal (deliver an auth code to the client)
1616
- * INSTEAD of `issue`/`redirect` — no browser session is minted.
1658
+ * tail to the `authz-consent` → `mint-authz-code` terminal (deliver an auth
1659
+ * code to the client) INSTEAD of `issue`/`redirect` — no browser session is
1660
+ * minted. `clientName`/`scope` are staged by `authz-consent` for the consent
1661
+ * form's display copy; `approved` is the explicit user-consent gate the
1662
+ * `mint-authz-code` terminal requires before it will mint a code.
1617
1663
  */
1618
1664
  authz?: {
1619
1665
  handle: string;
1666
+ clientName?: string;
1667
+ scope?: string;
1668
+ approved?: boolean;
1620
1669
  };
1621
1670
  /**
1622
1671
  * FE-facing surface — the ONLY top-level ctx key whitelisted on form
@@ -1672,7 +1721,8 @@ interface AuthWorkflowOpts {
1672
1721
  termsBump?: TAtscriptAnnotatedType;
1673
1722
  concurrencyLimit?: TAtscriptAnnotatedType;
1674
1723
  recoveryPincode?: TAtscriptAnnotatedType; /** Self-signup identifier form (`auth/signup/flow` entry pause). */
1675
- signup?: TAtscriptAnnotatedType;
1724
+ signup?: TAtscriptAnnotatedType; /** Consent prompt shown before `mint-authz-code` mints an auth code (AUTH-SERVER.md §6). */
1725
+ authzConsent?: TAtscriptAnnotatedType;
1676
1726
  };
1677
1727
  }
1678
1728
  /**
@@ -1722,6 +1772,7 @@ interface ResolvedAuthWorkflowOpts {
1722
1772
  concurrencyLimit: TAtscriptAnnotatedType;
1723
1773
  recoveryPincode: TAtscriptAnnotatedType;
1724
1774
  signup: TAtscriptAnnotatedType;
1775
+ authzConsent: TAtscriptAnnotatedType;
1725
1776
  };
1726
1777
  }
1727
1778
  //#endregion
@@ -2808,6 +2859,34 @@ declare class AuthWorkflow {
2808
2859
  * (cookies from `issue` are preserved).
2809
2860
  */
2810
2861
  redirect(ctx: AuthWfCtx): undefined;
2862
+ /**
2863
+ * Authorization-server consent gate (AUTH-SERVER.md §4.4 / §6). Runs BEFORE
2864
+ * `mint-authz-code` whenever `ctx.authz` is set. Two mandatory jobs:
2865
+ *
2866
+ * 1. **Browser binding** — verify the request carries the `aooth_authz`
2867
+ * cookie that constant-time-matches the secret recorded on the pending
2868
+ * authorization. An `authz` handle phished into a DIFFERENT browser (the
2869
+ * account-takeover primitive) fails here, because the secret lives only
2870
+ * in the browser that initiated `GET /auth/authorize`.
2871
+ * 2. **Explicit consent** — pause on the consent form and require the user
2872
+ * to press 'Authorize'. 'Deny' (or abandoning) 302s the client back with
2873
+ * `error=access_denied` and mints nothing. This blocks the same-browser
2874
+ * forced-navigation variant: a logged-in / silently-SSO-re-authenticated
2875
+ * victim is shown WHICH client is asking and must approve it.
2876
+ *
2877
+ * On approval it stamps `ctx.authz.approved`, which `mint-authz-code`
2878
+ * re-checks alongside a binding re-verification before minting.
2879
+ */
2880
+ authzConsent(ctx: AuthWfCtx): Promise<undefined>;
2881
+ /**
2882
+ * Constant-time match of the `aooth_authz` binding cookie against the secret
2883
+ * recorded on the pending authorization. Fail-closed (`false`) when the cookie
2884
+ * is absent or differs, so a request without the browser binding can never
2885
+ * redeem the handle. See {@link authzConsent}.
2886
+ */
2887
+ protected verifyAuthzBinding(req: {
2888
+ binding: string;
2889
+ }): boolean;
2811
2890
  /**
2812
2891
  * Authorization-server terminal (AUTH-SERVER.md §4.4). Reached INSTEAD of
2813
2892
  * `issue`/`redirect` when this login was started from `GET /auth/authorize`
@@ -2816,6 +2895,7 @@ declare class AuthWorkflow {
2816
2895
  * request's PKCE challenge / redirect / token policy, then 302s the browser to
2817
2896
  * `redirect_uri?code&state`. It does NOT issue a session and attaches NO
2818
2897
  * cookies — the token is minted later, off the browser, at `POST /auth/token`.
2898
+ * Gated by {@link authzConsent} (binding + explicit approval).
2819
2899
  */
2820
2900
  mintAuthzCode(ctx: AuthWfCtx): Promise<undefined>;
2821
2901
  /**
@@ -3106,4 +3186,4 @@ interface AuditEmitter {
3106
3186
  emit(event: AuditEvent): Promise<void> | void;
3107
3187
  }
3108
3188
  //#endregion
3109
- export { ADD_MFA_WORKFLOW, AUTH_CODE_STORE_TOKEN, type AuditEmitter, type AuditEvent, type AuthBindings, type AuthContext, AuthController, type AuthDeliveryPayload, type AuthEmailEvent, type AuthEmailKind, type AuthEmailOutletDeps, AuthGuarded, type AuthLoginResponse, type AuthLogoutBody, type AuthOkResponse, type AuthOptions, type AuthRefreshBody, type AuthSmsEvent, type AuthSmsKind, type AuthWfCompletionState, type AuthWfConsentsState, type AuthWfCtx, type AuthWfMfaEnrollState, type AuthWfOAuthState, type AuthWfPasswordUiState, type AuthWfPincodeUiState, AuthWorkflow, type AuthWorkflowOpts, AuthorizeController, AuthorizeRuntime, type BuildMagicLinkUrl, CHANGE_PASSWORD_WORKFLOW, CLIENT_REDIRECT_POLICY_TOKEN, type ConcurrencyLimitOptions, type ConnectedAccount, type ConsentDescriptor, type ConsentDescriptorLike, type ConsentEvent, ConsentStore, DEFAULT_AUTH_WORKFLOWS, type EmailSender, type EnrichedSession, FEDERATED_IDENTITY_STORE_TOKEN, type IssueResult, type LoginRedirect, type MfaSummary, type MfaTransport, OAUTH_CSRF_COOKIE, OAuthController, OAuthRuntime, PENDING_AUTHORIZATION_STORE_TOKEN, Public, RESERVED_USER_KEYS, type ResolvedAuthCookieConfig, type ResolvedAuthOptions, type ResolvedAuthWorkflowOpts, type SessionEnricher, SessionEnricherProvider, type SessionInfo, SessionsController, type SmsSender, type SsoProvider, type TAuthMeta, UserId, WfTrigger, type WfTriggerOpts, WfTriggerProvider, authGuardInterceptor, buildInviteAlreadyAcceptedEnvelope, createAuthEmailOutlet, deriveWfStateSecret, generateMagicLinkToken, getAuthMate, isSafeRelativeRedirect, oauthCsrfCookieAttrs, parseInviteRoles, resolveOAuthRedirect, stripReservedUserKeys, useAuth };
3189
+ export { ADD_MFA_WORKFLOW, AUTHZ_BINDING_COOKIE, AUTH_CODE_STORE_TOKEN, type AuditEmitter, type AuditEvent, type AuthBindings, type AuthContext, AuthController, type AuthDeliveryPayload, type AuthEmailEvent, type AuthEmailKind, type AuthEmailOutletDeps, AuthGuarded, type AuthLoginResponse, type AuthLogoutBody, type AuthOkResponse, type AuthOptions, type AuthRefreshBody, type AuthSmsEvent, type AuthSmsKind, type AuthWfCompletionState, type AuthWfConsentsState, type AuthWfCtx, type AuthWfMfaEnrollState, type AuthWfOAuthState, type AuthWfPasswordUiState, type AuthWfPincodeUiState, AuthWorkflow, type AuthWorkflowOpts, AuthorizeController, AuthorizeRuntime, type BuildMagicLinkUrl, CHANGE_PASSWORD_WORKFLOW, CLIENT_REDIRECT_POLICY_TOKEN, type ConcurrencyLimitOptions, type ConnectedAccount, type ConsentDescriptor, type ConsentDescriptorLike, type ConsentEvent, ConsentStore, DEFAULT_AUTH_WORKFLOWS, type EmailSender, type EnrichedSession, FEDERATED_IDENTITY_STORE_TOKEN, type IssueResult, type LoginRedirect, type MfaSummary, type MfaTransport, OAUTH_CSRF_COOKIE, OAuthController, OAuthRuntime, PENDING_AUTHORIZATION_STORE_TOKEN, Public, RESERVED_USER_KEYS, type ResolvedAuthCookieConfig, type ResolvedAuthOptions, type ResolvedAuthWorkflowOpts, type SessionEnricher, SessionEnricherProvider, type SessionInfo, SessionsController, type SmsSender, type SsoProvider, type TAuthMeta, UserId, WfTrigger, type WfTriggerOpts, WfTriggerProvider, authGuardInterceptor, authzBindingCookieAttrs, buildInviteAlreadyAcceptedEnvelope, createAuthEmailOutlet, deriveWfStateSecret, generateMagicLinkToken, getAuthMate, isSafeRelativeRedirect, oauthCsrfCookieAttrs, parseInviteRoles, resolveOAuthRedirect, stripReservedUserKeys, useAuth };
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import { C as SetPasswordForm, S as Select2faForm, T as TermsBumpForm, _ as ProveControlForm, a as EmailIdentifierForm, c as EnrollPickMethodForm, d as LoginCredentialsForm, g as PincodeForm, h as PasswordReauthForm, i as ConcurrencyLimitForm, l as EnrollTotpQrForm, m as MfaCodeForm, n as AskPhoneForm, o as EnrollAddressForm, p as ManageMfaForm, r as ChangePasswordForm, s as EnrollConfirmForm, t as AskEmailForm, u as InviteForm, v as ProveControlOtpForm, w as SignupForm, x as RemoveMfaConfirmForm } from "./forms-11EDZgS1.mjs";
1
+ import { C as Select2faForm, E as TermsBumpForm, S as RemoveMfaConfirmForm, T as SignupForm, _ as PincodeForm, a as ConcurrencyLimitForm, c as EnrollConfirmForm, d as InviteForm, f as LoginCredentialsForm, g as PasswordReauthForm, h as MfaCodeForm, i as ChangePasswordForm, l as EnrollPickMethodForm, m as ManageMfaForm, n as AskPhoneForm, o as EmailIdentifierForm, r as AuthorizeConsentForm, s as EnrollAddressForm, t as AskEmailForm, u as EnrollTotpQrForm, v as ProveControlForm, w as SetPasswordForm, y as ProveControlOtpForm } from "./forms-D7ZfanKT.mjs";
2
2
  import { Controller, HandlerPaths, Inherit, Inject, InjectMoostLogger, Injectable, Intercept, MoostInit, Optional, Param, Resolve, TInterceptorPriority, defineAfterInterceptor, defineBeforeInterceptor, getMoostMate, useControllerContext } from "moost";
3
3
  import { AuthCredential, AuthError, generateMagicLinkToken } from "@aooth/auth";
4
4
  import { current, defineWook, eventTypeKey, key } from "@wooksjs/event-core";
5
5
  import { HttpError, useAuthorization, useCookies, useHeaders, useRequest, useResponse, useUrlParams } from "@wooksjs/event-http";
6
6
  import { ArbacAction, ArbacResource, getArbacMate } from "@aooth/arbac-moost";
7
- import { FederatedIdentityStore, UserAuthError, UserService, generateTotpSecret, generateTotpUri, maskEmail, maskPhone, pickDefinedProfile, verifyTotpCode } from "@aooth/user";
7
+ import { FederatedIdentityStore, UserAuthError, UserService, generateMfaCode, generateTotpSecret, generateTotpUri, maskEmail, maskPhone, pickDefinedProfile, verifyTotpCode } from "@aooth/user";
8
8
  import { Body, Delete, Get, HttpError as HttpError$1, Post, Query } from "@moostjs/event-http";
9
- import { createHash, timingSafeEqual } from "node:crypto";
9
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
10
10
  import { createAsHttpOutlet, finishWf, handleAsOutletRequest, useAtscriptWf } from "@atscript/moost-wf";
11
11
  import { EncapsulatedStateStrategy, MoostWf, Step, StepRetriableError, StepTTL, Workflow, WorkflowParam, WorkflowSchema, createEmailOutlet, outletEmail, swapStrategy, useWfFinished, useWfState } from "@moostjs/event-wf";
12
12
  import { FederatedLoginService, OAuthError, OAuthProviderRegistry, generateRandomState, pkceChallengeFor } from "@aooth/idp";
@@ -585,6 +585,56 @@ function safeEqual(a, b) {
585
585
  return timingSafeEqual(ab, bb);
586
586
  }
587
587
  //#endregion
588
+ //#region src/authz/authz-binding.ts
589
+ /**
590
+ * Name of the double-submit cookie that binds an in-flight authorization
591
+ * request to the browser that started it. Set at `GET /auth/authorize` (holding
592
+ * the high-entropy `binding` secret recorded on the `PendingAuthorization`), and
593
+ * matched — constant-time, via `safeEqual` from `oauth-csrf` — at the
594
+ * `mint-authz-code` / `authz-consent` workflow steps before any code is minted.
595
+ *
596
+ * Why this closes the account-takeover (AUTH-SERVER.md §6): the opaque `authz`
597
+ * handle alone is a bearer ticket — anyone holding it can drive it to the
598
+ * code-minting terminal. An attacker who initiates `/auth/authorize` (to inject
599
+ * their own client / redirect / PKCE challenge) receives the handle AND this
600
+ * cookie in THEIR browser; phishing only the handle into a victim's browser
601
+ * fails the match here, because the victim's browser never holds the secret. So
602
+ * a handle can only be redeemed by the same browser that started the request.
603
+ */
604
+ const AUTHZ_BINDING_COOKIE = "aooth_authz";
605
+ /**
606
+ * Cookie attributes for the authorize-binding cookie — the same httpOnly /
607
+ * `SameSite=Lax` shape as the OAuth CSRF cookie (Lax, NOT Strict, so the
608
+ * top-level GET navigation BACK from a "Continue with <provider>" detour still
609
+ * carries it), so it delegates to {@link oauthCsrfCookieAttrs}. `maxAgeSec` is
610
+ * the pending authorization's remaining lifetime — the caller derives it from
611
+ * the row's `expiresAt` (returned by `PendingAuthorizationStore.create`), so the
612
+ * cookie tracks the row's TTL even when a store is configured with a non-default
613
+ * `ttlMs`. `secure` is caller-controlled (off for the http test harness, on in
614
+ * production).
615
+ */
616
+ function authzBindingCookieAttrs(opts) {
617
+ return oauthCsrfCookieAttrs({
618
+ secure: opts.secure,
619
+ maxAgeSec: opts.maxAgeSec
620
+ });
621
+ }
622
+ //#endregion
623
+ //#region src/authz/authz-redirect.ts
624
+ /**
625
+ * Build the client redirect URL for an authorization-server result (RFC 6749
626
+ * §4.1.2 / §4.1.2.1): append the result params (`code` on success, `error` on
627
+ * failure) and echo the client's `state` when it sent one. The single home for
628
+ * this URL contract — the controller's fail-soft redirect and the workflow's
629
+ * deny / code-mint terminals all build the same shape, only the transport
630
+ * (controller response vs `finishWf` envelope) differs.
631
+ */
632
+ function authzRedirectUrl(redirectUri, params) {
633
+ const url = new URL(redirectUri);
634
+ for (const [key, value] of Object.entries(params)) if (value !== void 0) url.searchParams.set(key, value);
635
+ return url.toString();
636
+ }
637
+ //#endregion
588
638
  //#region src/oauth/oauth-redirect.ts
589
639
  /**
590
640
  * Open-redirect defense for the post-login `redirect` carried across the OAuth
@@ -899,7 +949,8 @@ const DEFAULT_FORMS = {
899
949
  termsBump: TermsBumpForm,
900
950
  concurrencyLimit: ConcurrencyLimitForm,
901
951
  recoveryPincode: PincodeForm,
902
- signup: SignupForm
952
+ signup: SignupForm,
953
+ authzConsent: AuthorizeConsentForm
903
954
  };
904
955
  function mergeAuthWorkflowOpts(opts) {
905
956
  return {
@@ -1699,6 +1750,10 @@ let AuthWorkflow = class AuthWorkflow {
1699
1750
  if (sub) pub.proveControl = sub;
1700
1751
  }
1701
1752
  if (ctx.newPasswordRequired !== void 0) pub.newPasswordRequired = ctx.newPasswordRequired;
1753
+ if (ctx.authz) {
1754
+ const sub = pickDefined(ctx.authz, ["clientName", "scope"]);
1755
+ if (sub) pub.authz = sub;
1756
+ }
1702
1757
  ctx.public = pub;
1703
1758
  }
1704
1759
  /**
@@ -1735,8 +1790,7 @@ let AuthWorkflow = class AuthWorkflow {
1735
1790
  }
1736
1791
  /** Mint a numeric pincode + stash it onto ctx. Returns the plain code. */
1737
1792
  mintPin(ctx, length, ttlMs) {
1738
- let code = "";
1739
- for (let i = 0; i < length; i++) code += Math.floor(Math.random() * 10).toString();
1793
+ const code = generateMfaCode(length);
1740
1794
  ctx.pin = code;
1741
1795
  ctx.pinExpire = Date.now() + ttlMs;
1742
1796
  delete ctx.pinAttempts;
@@ -3605,6 +3659,74 @@ let AuthWorkflow = class AuthWorkflow {
3605
3659
  (ctx.completion ??= {}).redirectUrl = url;
3606
3660
  }
3607
3661
  /**
3662
+ * Authorization-server consent gate (AUTH-SERVER.md §4.4 / §6). Runs BEFORE
3663
+ * `mint-authz-code` whenever `ctx.authz` is set. Two mandatory jobs:
3664
+ *
3665
+ * 1. **Browser binding** — verify the request carries the `aooth_authz`
3666
+ * cookie that constant-time-matches the secret recorded on the pending
3667
+ * authorization. An `authz` handle phished into a DIFFERENT browser (the
3668
+ * account-takeover primitive) fails here, because the secret lives only
3669
+ * in the browser that initiated `GET /auth/authorize`.
3670
+ * 2. **Explicit consent** — pause on the consent form and require the user
3671
+ * to press 'Authorize'. 'Deny' (or abandoning) 302s the client back with
3672
+ * `error=access_denied` and mints nothing. This blocks the same-browser
3673
+ * forced-navigation variant: a logged-in / silently-SSO-re-authenticated
3674
+ * victim is shown WHICH client is asking and must approve it.
3675
+ *
3676
+ * On approval it stamps `ctx.authz.approved`, which `mint-authz-code`
3677
+ * re-checks alongside a binding re-verification before minting.
3678
+ */
3679
+ async authzConsent(ctx) {
3680
+ this.requireSubject(ctx);
3681
+ const authz = ctx.authz;
3682
+ const { pending } = await this.authorizeRuntime();
3683
+ const req = authz ? await pending.get(authz.handle) : null;
3684
+ if (!authz || !req) {
3685
+ finishWf({ message: {
3686
+ level: "error",
3687
+ text: "Authorization request expired. Please try again."
3688
+ } });
3689
+ return;
3690
+ }
3691
+ if (!this.verifyAuthzBinding(req)) {
3692
+ await pending.delete(authz.handle);
3693
+ finishWf({ message: {
3694
+ level: "error",
3695
+ text: "This authorization could not be verified for your browser. Please start again."
3696
+ } });
3697
+ return;
3698
+ }
3699
+ if (req.clientId !== void 0) authz.clientName = req.clientId;
3700
+ if (req.scope !== void 0) authz.scope = req.scope;
3701
+ const wf = this.useAtscriptWfPublic(ctx, this.opts.forms.authzConsent);
3702
+ if (wf.resolveAction() === "deny") {
3703
+ await pending.delete(authz.handle);
3704
+ finishWf({ next: {
3705
+ trigger: "immediate",
3706
+ action: {
3707
+ type: "redirect",
3708
+ target: authzRedirectUrl(req.redirectUri, {
3709
+ error: "access_denied",
3710
+ state: req.clientState
3711
+ }),
3712
+ reason: "authz-denied"
3713
+ }
3714
+ } });
3715
+ return;
3716
+ }
3717
+ wf.resolveInput();
3718
+ authz.approved = true;
3719
+ }
3720
+ /**
3721
+ * Constant-time match of the `aooth_authz` binding cookie against the secret
3722
+ * recorded on the pending authorization. Fail-closed (`false`) when the cookie
3723
+ * is absent or differs, so a request without the browser binding can never
3724
+ * redeem the handle. See {@link authzConsent}.
3725
+ */
3726
+ verifyAuthzBinding(req) {
3727
+ return safeEqual(useCookies(current()).getCookie(AUTHZ_BINDING_COOKIE), req.binding);
3728
+ }
3729
+ /**
3608
3730
  * Authorization-server terminal (AUTH-SERVER.md §4.4). Reached INSTEAD of
3609
3731
  * `issue`/`redirect` when this login was started from `GET /auth/authorize`
3610
3732
  * (`ctx.authz` set by `init-login` or re-raised by `sso-callback`). Mints a
@@ -3612,6 +3734,7 @@ let AuthWorkflow = class AuthWorkflow {
3612
3734
  * request's PKCE challenge / redirect / token policy, then 302s the browser to
3613
3735
  * `redirect_uri?code&state`. It does NOT issue a session and attaches NO
3614
3736
  * cookies — the token is minted later, off the browser, at `POST /auth/token`.
3737
+ * Gated by {@link authzConsent} (binding + explicit approval).
3615
3738
  */
3616
3739
  async mintAuthzCode(ctx) {
3617
3740
  this.requireSubject(ctx);
@@ -3625,6 +3748,14 @@ let AuthWorkflow = class AuthWorkflow {
3625
3748
  } });
3626
3749
  return;
3627
3750
  }
3751
+ if (ctx.authz?.approved !== true || !this.verifyAuthzBinding(req)) {
3752
+ await pending.delete(handle);
3753
+ finishWf({ message: {
3754
+ level: "error",
3755
+ text: "Authorization could not be completed. Please start again."
3756
+ } });
3757
+ return;
3758
+ }
3628
3759
  const { code } = await codes.mint({
3629
3760
  userId: ctx.subject,
3630
3761
  codeChallenge: req.codeChallenge,
@@ -3638,14 +3769,14 @@ let AuthWorkflow = class AuthWorkflow {
3638
3769
  tokenPolicy: req.tokenPolicy
3639
3770
  });
3640
3771
  await pending.delete(handle);
3641
- const url = new URL(req.redirectUri);
3642
- url.searchParams.set("code", code);
3643
- if (req.clientState !== void 0) url.searchParams.set("state", req.clientState);
3644
3772
  finishWf({ next: {
3645
3773
  trigger: "immediate",
3646
3774
  action: {
3647
3775
  type: "redirect",
3648
- target: url.toString(),
3776
+ target: authzRedirectUrl(req.redirectUri, {
3777
+ code,
3778
+ state: req.clientState
3779
+ }),
3649
3780
  reason: "authz-code"
3650
3781
  }
3651
3782
  } });
@@ -4841,6 +4972,14 @@ __decorate([
4841
4972
  __decorateMetadata("design:paramtypes", [Object]),
4842
4973
  __decorateMetadata("design:returntype", void 0)
4843
4974
  ], AuthWorkflow.prototype, "redirect", null);
4975
+ __decorate([
4976
+ Step("authz-consent"),
4977
+ Public(),
4978
+ __decorateParam(0, WorkflowParam("context")),
4979
+ __decorateMetadata("design:type", Function),
4980
+ __decorateMetadata("design:paramtypes", [Object]),
4981
+ __decorateMetadata("design:returntype", Promise)
4982
+ ], AuthWorkflow.prototype, "authzConsent", null);
4844
4983
  __decorate([
4845
4984
  Step("mint-authz-code"),
4846
4985
  Public(),
@@ -5033,8 +5172,12 @@ __decorate([
5033
5172
  ]
5034
5173
  },
5035
5174
  {
5036
- id: "mint-authz-code",
5175
+ id: "authz-consent",
5037
5176
  condition: (ctx) => !!ctx.authz
5177
+ },
5178
+ {
5179
+ id: "mint-authz-code",
5180
+ condition: (ctx) => ctx.authz?.approved === true
5038
5181
  }
5039
5182
  ]),
5040
5183
  __decorateMetadata("design:type", Function),
@@ -5844,7 +5987,8 @@ let AuthorizeController = class AuthorizeController {
5844
5987
  return e instanceof AuthorizeError ? `invalid request: ${e.code}` : "invalid request";
5845
5988
  }
5846
5989
  if (responseType !== "code" || !codeChallenge || codeChallengeMethod !== "S256") return this.redirectError(resolved.redirectUri, "invalid_request", state);
5847
- const { handle } = await this.pending.create({
5990
+ const binding = randomBytes(32).toString("base64url");
5991
+ const { handle, expiresAt } = await this.pending.create({
5848
5992
  ...resolved.clientId !== void 0 && { clientId: resolved.clientId },
5849
5993
  redirectUri: resolved.redirectUri,
5850
5994
  codeChallenge,
@@ -5854,8 +5998,14 @@ let AuthorizeController = class AuthorizeController {
5854
5998
  ...resolved.idToken !== void 0 && { idToken: resolved.idToken },
5855
5999
  ...resolved.accessToken !== void 0 && { accessToken: resolved.accessToken },
5856
6000
  ...resolved.audience !== void 0 && { audience: resolved.audience },
5857
- tokenPolicy: resolved.tokenPolicy
6001
+ tokenPolicy: resolved.tokenPolicy,
6002
+ binding
5858
6003
  });
6004
+ const maxAgeSec = Math.max(1, Math.round((expiresAt - Date.now()) / 1e3));
6005
+ res.setCookie(AUTHZ_BINDING_COOKIE, binding, authzBindingCookieAttrs({
6006
+ secure: useAuth().options.cookie.secure,
6007
+ maxAgeSec
6008
+ }));
5859
6009
  const loginPath = this.loginPath();
5860
6010
  const target = `${loginPath}${loginPath.includes("?") ? "&" : "?"}authz=${encodeURIComponent(handle)}`;
5861
6011
  res.status = 302;
@@ -5990,12 +6140,12 @@ let AuthorizeController = class AuthorizeController {
5990
6140
  }
5991
6141
  /** Fail soft: 302 the validated client redirect with an `?error=` (+ echoed `state`). */
5992
6142
  redirectError(redirectUri, error, state) {
5993
- const url = new URL(redirectUri);
5994
- url.searchParams.set("error", error);
5995
- if (state !== void 0) url.searchParams.set("state", state);
5996
6143
  const res = useResponse(current());
5997
6144
  res.status = 302;
5998
- res.setHeader("Location", url.toString());
6145
+ res.setHeader("Location", authzRedirectUrl(redirectUri, {
6146
+ error,
6147
+ state
6148
+ }));
5999
6149
  return "";
6000
6150
  }
6001
6151
  };
@@ -6094,4 +6244,4 @@ function createAuthEmailOutlet(deps) {
6094
6244
  });
6095
6245
  }
6096
6246
  //#endregion
6097
- export { ADD_MFA_WORKFLOW, AUTH_CODE_STORE_TOKEN, AuthController, AuthGuarded, AuthWorkflow, AuthorizeController, AuthorizeRuntime, CHANGE_PASSWORD_WORKFLOW, CLIENT_REDIRECT_POLICY_TOKEN, ConsentStore, DEFAULT_AUTH_WORKFLOWS, FEDERATED_IDENTITY_STORE_TOKEN, OAUTH_CSRF_COOKIE, OAuthController, OAuthRuntime, PENDING_AUTHORIZATION_STORE_TOKEN, Public, RESERVED_USER_KEYS, SessionEnricherProvider, SessionsController, UserId, WfTrigger, WfTriggerProvider, authGuardInterceptor, buildInviteAlreadyAcceptedEnvelope, createAuthEmailOutlet, deriveWfStateSecret, generateMagicLinkToken, getAuthMate, isSafeRelativeRedirect, oauthCsrfCookieAttrs, parseInviteRoles, resolveOAuthRedirect, stripReservedUserKeys, useAuth };
6247
+ export { ADD_MFA_WORKFLOW, AUTHZ_BINDING_COOKIE, AUTH_CODE_STORE_TOKEN, AuthController, AuthGuarded, AuthWorkflow, AuthorizeController, AuthorizeRuntime, CHANGE_PASSWORD_WORKFLOW, CLIENT_REDIRECT_POLICY_TOKEN, ConsentStore, DEFAULT_AUTH_WORKFLOWS, FEDERATED_IDENTITY_STORE_TOKEN, OAUTH_CSRF_COOKIE, OAuthController, OAuthRuntime, PENDING_AUTHORIZATION_STORE_TOKEN, Public, RESERVED_USER_KEYS, SessionEnricherProvider, SessionsController, UserId, WfTrigger, WfTriggerProvider, authGuardInterceptor, authzBindingCookieAttrs, buildInviteAlreadyAcceptedEnvelope, createAuthEmailOutlet, deriveWfStateSecret, generateMagicLinkToken, getAuthMate, isSafeRelativeRedirect, oauthCsrfCookieAttrs, parseInviteRoles, resolveOAuthRedirect, stripReservedUserKeys, useAuth };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/auth-moost",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Moost auth integration for aoothjs — AuthGuard interceptor, useAuth composable, REST endpoints, workflows",
5
5
  "keywords": [
6
6
  "aoothjs",
@@ -57,33 +57,33 @@
57
57
  "access": "public"
58
58
  },
59
59
  "dependencies": {
60
- "@atscript/moost-wf": "^0.1.97",
61
- "@wooksjs/http-body": "^0.7.17",
62
- "@aooth/arbac-moost": "^0.1.16",
63
- "@aooth/auth": "0.1.16",
64
- "@aooth/idp": "0.1.16",
65
- "@aooth/user": "0.1.16"
60
+ "@atscript/moost-wf": "^0.1.98",
61
+ "@wooksjs/http-body": "^0.7.19",
62
+ "@aooth/arbac-moost": "^0.1.18",
63
+ "@aooth/auth": "0.1.18",
64
+ "@aooth/idp": "0.1.18",
65
+ "@aooth/user": "0.1.18"
66
66
  },
67
67
  "devDependencies": {
68
- "@atscript/core": "^0.1.74",
69
- "@atscript/typescript": "^0.1.74",
70
- "@atscript/ui": "^0.1.97",
71
- "@atscript/ui-fns": "^0.1.97",
72
- "@moostjs/event-http": "^0.6.25",
73
- "@moostjs/event-wf": "^0.6.25",
74
- "moost": "^0.6.25",
75
- "unplugin-atscript": "^0.1.74",
76
- "wooks": "^0.7.17"
68
+ "@atscript/core": "^0.1.75",
69
+ "@atscript/typescript": "^0.1.75",
70
+ "@atscript/ui": "^0.1.98",
71
+ "@atscript/ui-fns": "^0.1.98",
72
+ "@moostjs/event-http": "^0.6.26",
73
+ "@moostjs/event-wf": "^0.6.26",
74
+ "moost": "^0.6.26",
75
+ "unplugin-atscript": "^0.1.75",
76
+ "wooks": "^0.7.19"
77
77
  },
78
78
  "peerDependencies": {
79
- "@atscript/moost-wf": "^0.1.97",
80
- "@atscript/typescript": "^0.1.74",
81
- "@moostjs/event-http": "^0.6.25",
82
- "@moostjs/event-wf": "^0.6.25",
83
- "@wooksjs/event-core": "^0.7.17",
84
- "@wooksjs/event-http": "^0.7.17",
85
- "@wooksjs/http-body": "^0.7.17",
86
- "moost": "^0.6.25"
79
+ "@atscript/moost-wf": "^0.1.98",
80
+ "@atscript/typescript": "^0.1.75",
81
+ "@moostjs/event-http": "^0.6.26",
82
+ "@moostjs/event-wf": "^0.6.26",
83
+ "@wooksjs/event-core": "^0.7.19",
84
+ "@wooksjs/event-http": "^0.7.19",
85
+ "@wooksjs/http-body": "^0.7.19",
86
+ "moost": "^0.6.26"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@atscript/typescript": {
@@ -95,7 +95,6 @@
95
95
  "build": "vp pack",
96
96
  "dev": "vp pack --watch",
97
97
  "test": "vp test",
98
- "check": "vp check",
99
- "postinstall": "asc"
98
+ "check": "vp check"
100
99
  }
101
100
  }
@@ -258,7 +258,7 @@ export interface SignupForm {
258
258
  * wants to abandon the flow closes / refreshes the page (the wf state token
259
259
  * expires per the engine's TTL).
260
260
  */
261
- @ui.form.fn.title '(_, _d, ctx) => ctx.public?.password?.heading || "Set your password"'
261
+ @ui.form.fn.title '(_, ctx) => ctx.public?.password?.heading || "Set your password"'
262
262
  @wf.context.pass 'public'
263
263
  export interface SetPasswordForm extends WithInlineConsentForm {
264
264
  /**
@@ -537,7 +537,7 @@ export interface EnrollPickMethodForm {
537
537
  * forbids backing out mid-flow). `useDifferentMethod` is hidden when the
538
538
  * consumer has only one transport configured (nothing to switch to).
539
539
  */
540
- @ui.form.fn.title '(_, _d, ctx) => ctx.public?.mfaEnroll?.method === "sms" ? "Add your phone number" : "Add your email"'
540
+ @ui.form.fn.title '(_, ctx) => ctx.public?.mfaEnroll?.method === "sms" ? "Add your phone number" : "Add your email"'
541
541
  @meta.description 'We will send you a one-time code to confirm.'
542
542
  @wf.context.pass 'public'
543
543
  @ui.form.submit.text 'Send code'
@@ -849,7 +849,7 @@ export interface RecoveryFactorForm {
849
849
  * `SetPasswordForm`, so the live `AsPasswordRules` renderer and dynamic copy
850
850
  * work identically. Heading/intro are staged by `change-password-form`.
851
851
  */
852
- @ui.form.fn.title '(_, _d, ctx) => ctx.public?.password?.heading || "Change your password"'
852
+ @ui.form.fn.title '(_, ctx) => ctx.public?.password?.heading || "Change your password"'
853
853
  @ui.form.submit.text 'Change password'
854
854
  @wf.context.pass 'public'
855
855
  export interface ChangePasswordForm {
@@ -982,3 +982,29 @@ export interface ProveControlOtpForm {
982
982
  @ui.form.pushDown
983
983
  cancel?: ui.action
984
984
  }
985
+
986
+ /**
987
+ * Authorization-server consent prompt (AUTH-SERVER.md §4.4 / §6). Rendered by
988
+ * the `authz-consent` @Step once the human is authenticated, BEFORE
989
+ * `mint-authz-code` mints the authorization code — so a logged-in (or silently
990
+ * SSO-re-authenticated) browser cannot be walked into delivering a code to a
991
+ * client it never approved. Fieldless apart from the explanatory paragraph; the
992
+ * primary submit ('Authorize') records consent and proceeds to the mint, the
993
+ * 'Deny' action 302s the client back with `error=access_denied`. The requesting
994
+ * client + scope ride the `@wf.context.pass 'public'` `ctx.public.authz`
995
+ * whitelist (display-only — the handle / approval gate stay server-side).
996
+ */
997
+ @meta.label 'Authorize access'
998
+ @wf.context.pass 'public'
999
+ @ui.form.submit.text 'Authorize'
1000
+ export interface AuthorizeConsentForm {
1001
+ @ui.form.order 1
1002
+ @ui.form.fn.value '(_, _d, ctx) => { const a = ctx.public?.authz; const who = a?.clientName ? "“" + a.clientName + "”" : "A local application"; const sc = a?.scope ? " It is requesting access to: " + a.scope + "." : ""; return who + " wants to sign in to your account." + sc + " Authorize this only if you started it."; }'
1003
+ notice: ui.paragraph
1004
+
1005
+ @ui.form.order 10
1006
+ @ui.form.action 'deny', 'Deny'
1007
+ @ui.form.attr 'align', 'center'
1008
+ @ui.form.pushDown
1009
+ deny?: ui.action
1010
+ }
@@ -471,4 +471,21 @@ export declare class ProveControlOtpForm {
471
471
  /** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */
472
472
  static toExampleData?: () => any
473
473
  }
474
+
475
+ /**
476
+ * Atscript interface **AuthorizeConsentForm**
477
+ * @see {@link ./forms.as:1000:18}
478
+ */
479
+ export declare class AuthorizeConsentForm {
480
+ // notice: ui.paragraph
481
+ // deny: ui.action
482
+ static __is_atscript_annotated_type: true
483
+ static type: TAtscriptTypeObject<keyof AuthorizeConsentForm, AuthorizeConsentForm>
484
+ static metadata: TMetadataMap<AtscriptMetadata>
485
+ static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof AuthorizeConsentForm>
486
+ /** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */
487
+ static toJsonSchema: () => any
488
+ /** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */
489
+ static toExampleData?: () => any
490
+ }
474
491
  // prettier-ignore-end