@aooth/auth-moost 0.1.30 → 0.1.31
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/dist/index.d.mts +92 -6
- package/dist/index.mjs +116 -25
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -1879,6 +1879,28 @@ interface AuthWfCtx {
|
|
|
1879
1879
|
interface AuthWorkflowOpts {
|
|
1880
1880
|
autoLoginOnInvite?: boolean;
|
|
1881
1881
|
autoLoginOnRecover?: boolean;
|
|
1882
|
+
/**
|
|
1883
|
+
* Closed universe of role ids the admin invite form may assign. NOT policy
|
|
1884
|
+
* (it does not vary per request) — it is static configuration, like `forms`.
|
|
1885
|
+
* The base `getAvailableRoles()` intersects this list with the CURRENT
|
|
1886
|
+
* inviter's ARBAC grants (`auth.invite` / `assign:<role>`) at request time,
|
|
1887
|
+
* so each inviter is offered — and server-side limited to — only the roles
|
|
1888
|
+
* they may actually delegate. If ARBAC is unreachable the list is used
|
|
1889
|
+
* verbatim (still a CLOSED whitelist, never fail-open).
|
|
1890
|
+
*
|
|
1891
|
+
* Leave unset to keep the legacy behaviour (NO whitelist — any role can be
|
|
1892
|
+
* assigned). When unset, `getAvailableRoles` is not overridden, and
|
|
1893
|
+
* `allowAnyInviteRole` is not set, a one-time warning fires the first time
|
|
1894
|
+
* the admin invite flow runs.
|
|
1895
|
+
*/
|
|
1896
|
+
invitableRoles?: string[];
|
|
1897
|
+
/**
|
|
1898
|
+
* Acknowledge an intentionally-unrestricted invite form — silences the
|
|
1899
|
+
* "invite role whitelist is OFF" warning when `invitableRoles` is unset and
|
|
1900
|
+
* `getAvailableRoles` is not overridden. Records intent only; it does not by
|
|
1901
|
+
* itself relax or tighten enforcement. Default `false`.
|
|
1902
|
+
*/
|
|
1903
|
+
allowAnyInviteRole?: boolean;
|
|
1882
1904
|
/** Pincode infrastructure shared by login MFA, invite MFA, and recovery OTP. */
|
|
1883
1905
|
mfa?: {
|
|
1884
1906
|
pincodeLength?: number;
|
|
@@ -1952,6 +1974,8 @@ interface AuthWorkflowOpts {
|
|
|
1952
1974
|
interface ResolvedAuthWorkflowOpts {
|
|
1953
1975
|
autoLoginOnInvite: boolean;
|
|
1954
1976
|
autoLoginOnRecover: boolean;
|
|
1977
|
+
invitableRoles: string[];
|
|
1978
|
+
allowAnyInviteRole: boolean;
|
|
1955
1979
|
mfa: {
|
|
1956
1980
|
pincodeLength: number;
|
|
1957
1981
|
pincodeTtlMs: number;
|
|
@@ -2111,6 +2135,12 @@ declare class AuthWorkflow {
|
|
|
2111
2135
|
protected readonly users: UserService;
|
|
2112
2136
|
protected readonly auth: AuthCredential;
|
|
2113
2137
|
protected readonly consentStore: ConsentStore;
|
|
2138
|
+
/**
|
|
2139
|
+
* Process-lifetime warn-once latch for the "invite role whitelist is OFF"
|
|
2140
|
+
* notice. App-level (singleton) state, NOT per-event — keeps the warning from
|
|
2141
|
+
* spamming on every invite while still surfacing the misconfig once.
|
|
2142
|
+
*/
|
|
2143
|
+
private warnedOpenInviteWhitelist;
|
|
2114
2144
|
constructor(opts: Partial<AuthWorkflowOpts>, users: UserService, auth: AuthCredential, consentStore: ConsentStore);
|
|
2115
2145
|
/**
|
|
2116
2146
|
* Unified outbound dispatch hook for direct synchronous deliveries
|
|
@@ -2174,12 +2204,39 @@ declare class AuthWorkflow {
|
|
|
2174
2204
|
*/
|
|
2175
2205
|
protected afterMfaChanged(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2176
2206
|
/**
|
|
2177
|
-
*
|
|
2178
|
-
*
|
|
2179
|
-
* `
|
|
2180
|
-
*
|
|
2207
|
+
* Selectable role ids for the admin invite form — ALSO enforced server-side
|
|
2208
|
+
* (`admin-form` rejects any submitted role outside this set). Read by
|
|
2209
|
+
* `prepareAvailableRoles`. Mirrors the prior `InviteWorkflow.getAvailableRoles()`
|
|
2210
|
+
* consumer hook.
|
|
2211
|
+
*
|
|
2212
|
+
* Default behaviour is driven by {@link AuthWorkflowOpts.invitableRoles}:
|
|
2213
|
+
* - **configured** → that universe intersected with the CURRENT inviter's
|
|
2214
|
+
* ARBAC grants (`auth.invite` / `assign:<role>`), so an inviter can only
|
|
2215
|
+
* delegate roles they may themselves assign. If ARBAC is unreachable the
|
|
2216
|
+
* universe is returned verbatim — still a CLOSED whitelist, never fail-open.
|
|
2217
|
+
* - **unset** → `undefined`, preserving the legacy "no whitelist" behaviour;
|
|
2218
|
+
* `prepareAvailableRoles` warns once unless
|
|
2219
|
+
* {@link AuthWorkflowOpts.allowAnyInviteRole} acknowledges it.
|
|
2220
|
+
*
|
|
2221
|
+
* Override for fully custom sourcing — the override then owns the gate
|
|
2222
|
+
* outright (no warning fires; `invitableRoles` is consulted only if the
|
|
2223
|
+
* override reads it).
|
|
2181
2224
|
*/
|
|
2182
2225
|
protected getAvailableRoles(): Promise<string[] | undefined> | string[] | undefined;
|
|
2226
|
+
/**
|
|
2227
|
+
* Intersect a role universe with the current inviter's ARBAC grants — keep
|
|
2228
|
+
* only roles they hold `auth.invite` / `assign:<role>` for. Falls back to the
|
|
2229
|
+
* universe verbatim when ARBAC is unreachable (no event context / not wired),
|
|
2230
|
+
* which keeps the whitelist CLOSED rather than failing open.
|
|
2231
|
+
*/
|
|
2232
|
+
protected filterInvitableRolesByArbac(universe: string[]): Promise<string[]>;
|
|
2233
|
+
/**
|
|
2234
|
+
* True when the admin invite form would assign roles with NO server-side
|
|
2235
|
+
* whitelist in effect: `invitableRoles` unset, `getAvailableRoles` not
|
|
2236
|
+
* overridden, and the open default not acknowledged via `allowAnyInviteRole`.
|
|
2237
|
+
* Drives the one-time `prepare-available-roles` warning.
|
|
2238
|
+
*/
|
|
2239
|
+
protected inviteWhitelistIsOpen(): boolean;
|
|
2183
2240
|
/**
|
|
2184
2241
|
* Build the extras dict merged into the freshly-created user row. Runs for
|
|
2185
2242
|
* EVERY new-account path: password-signup and invite-accept merge it at
|
|
@@ -2494,6 +2551,21 @@ declare class AuthWorkflow {
|
|
|
2494
2551
|
* `AuthWorkflowOpts.autoLoginOnInvite` boolean (per §2 decision).
|
|
2495
2552
|
*/
|
|
2496
2553
|
protected resolveAccept(_ctx: AuthWfCtx): NonNullable<AuthWfCtx["accept"]> | Promise<NonNullable<AuthWfCtx["accept"]>>;
|
|
2554
|
+
/**
|
|
2555
|
+
* Copy (heading + intro) for the unified set-password screen, staged by
|
|
2556
|
+
* `create-password-form` BEFORE the pause. Branches on the resolved
|
|
2557
|
+
* `ctx.password.changeReason` (`expired` / `reset`), then invite-accept
|
|
2558
|
+
* (`ctx.accept`), then the initial-password fallback. Override to re-brand any
|
|
2559
|
+
* phase; return a partial (`{ heading }` only) to change one field and leave
|
|
2560
|
+
* the other at whatever an earlier step staged, or `{}` to keep both as-is.
|
|
2561
|
+
*/
|
|
2562
|
+
protected resolveSetPasswordCopy(ctx: AuthWfCtx): {
|
|
2563
|
+
heading?: string;
|
|
2564
|
+
intro?: string;
|
|
2565
|
+
} | Promise<{
|
|
2566
|
+
heading?: string;
|
|
2567
|
+
intro?: string;
|
|
2568
|
+
}>;
|
|
2497
2569
|
/**
|
|
2498
2570
|
* Resolve the recovery post-reset policy. Reached from recovery.flow.
|
|
2499
2571
|
* `freshLoginRequired` REMOVED — the auto-login choice is the static
|
|
@@ -2920,11 +2992,25 @@ declare class AuthWorkflow {
|
|
|
2920
2992
|
preparePasswordRules(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2921
2993
|
preparePostReset(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2922
2994
|
prepareRecoveryAltActions(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2995
|
+
/**
|
|
2996
|
+
* Roles the server will honor from the admin invite form. SECURITY boundary:
|
|
2997
|
+
* when `resolveAdminForm` returned `collectRoles: false` the role picker is
|
|
2998
|
+
* hidden, so any submitted `roles[]` is a crafted or buggy payload and is
|
|
2999
|
+
* IGNORED — roles in that mode come from `inferAdminRoles` (server-side),
|
|
3000
|
+
* never client input. This keeps role authorization independent of whether
|
|
3001
|
+
* `prepare-available-roles` ran: that step populates the `availableRoles`
|
|
3002
|
+
* whitelist the `admin-form` guard enforces against, but it is schema-gated on
|
|
3003
|
+
* `collectRoles`, so WITHOUT this check a `collectRoles:false` deployment
|
|
3004
|
+
* would let a crafted POST assign ANY role with no whitelist / per-role ARBAC
|
|
3005
|
+
* `assign:<role>` check.
|
|
3006
|
+
*/
|
|
3007
|
+
protected effectiveInviteRoles(ctx: AuthWfCtx, submitted: string[]): string[];
|
|
2923
3008
|
/**
|
|
2924
3009
|
* Admin-side invite form. Pauses for `InviteForm`; binds `ctx.email` +
|
|
2925
3010
|
* `ctx.admin.roles`. Server-side enforces the `availableRoles` whitelist
|
|
2926
|
-
* (populated by `prepare-available-roles`)
|
|
2927
|
-
*
|
|
3011
|
+
* (populated by `prepare-available-roles`) and, via {@link effectiveInviteRoles},
|
|
3012
|
+
* ignores any submitted roles when `resolveAdminForm` set `collectRoles:false`.
|
|
3013
|
+
* Calls `duplicateInviteCheck` to decide whether to reject duplicates.
|
|
2928
3014
|
*/
|
|
2929
3015
|
adminForm(ctx: AuthWfCtx): Promise<unknown>;
|
|
2930
3016
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { C as Select2faForm, D as TermsBumpForm, E as StepUpConfirmForm, 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-xaBNc5Ng.mjs";
|
|
2
|
-
import { Controller, HandlerPaths, Inherit, Inject, InjectMoostLogger, Injectable, Intercept, MoostInit, Optional, Param, Resolve, TInterceptorPriority, defineAfterInterceptor, defineBeforeInterceptor, getMoostMate, useControllerContext } from "moost";
|
|
2
|
+
import { Controller, HandlerPaths, Inherit, Inject, InjectMoostLogger, Injectable, Intercept, MoostInit, Optional, Param, Resolve, TInterceptorPriority, defineAfterInterceptor, defineBeforeInterceptor, getMoostMate, useControllerContext, useLogger } 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
|
-
import { ArbacAction, ArbacResource, getArbacMate } from "@aooth/arbac-moost";
|
|
6
|
+
import { ArbacAction, ArbacResource, getArbacMate, useArbac } from "@aooth/arbac-moost";
|
|
7
7
|
import { FederatedIdentityStore, SEEN_DEVICES_DEFAULT_CAP, 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
9
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -942,6 +942,7 @@ const consentsPersistTailSchema = [{
|
|
|
942
942
|
* is registerable as a Moost controller, but running any of its `@Workflow`
|
|
943
943
|
* schemas would no-op every step.
|
|
944
944
|
*/
|
|
945
|
+
var _AuthWorkflow;
|
|
945
946
|
/**
|
|
946
947
|
* Default form schemas — the bundled `forms.as` models the workflow's
|
|
947
948
|
* `useAtscriptWf()` callers resolve to. Consumers can override any field
|
|
@@ -988,6 +989,8 @@ function mergeAuthWorkflowOpts(opts) {
|
|
|
988
989
|
return {
|
|
989
990
|
autoLoginOnInvite: opts.autoLoginOnInvite ?? true,
|
|
990
991
|
autoLoginOnRecover: opts.autoLoginOnRecover ?? false,
|
|
992
|
+
invitableRoles: opts.invitableRoles ?? [],
|
|
993
|
+
allowAnyInviteRole: opts.allowAnyInviteRole ?? false,
|
|
991
994
|
mfa: {
|
|
992
995
|
pincodeLength: opts.mfa?.pincodeLength ?? 6,
|
|
993
996
|
pincodeTtlMs: opts.mfa?.pincodeTtlMs ?? 300 * 1e3,
|
|
@@ -1197,11 +1200,17 @@ function pickDefined(src, keys) {
|
|
|
1197
1200
|
* so enumeration is moot there).
|
|
1198
1201
|
*/
|
|
1199
1202
|
var RecoveryMethodUnavailableError = class extends Error {};
|
|
1200
|
-
let AuthWorkflow = class AuthWorkflow {
|
|
1203
|
+
let AuthWorkflow = _AuthWorkflow = class AuthWorkflow {
|
|
1201
1204
|
opts;
|
|
1202
1205
|
users;
|
|
1203
1206
|
auth;
|
|
1204
1207
|
consentStore;
|
|
1208
|
+
/**
|
|
1209
|
+
* Process-lifetime warn-once latch for the "invite role whitelist is OFF"
|
|
1210
|
+
* notice. App-level (singleton) state, NOT per-event — keeps the warning from
|
|
1211
|
+
* spamming on every invite while still surfacing the misconfig once.
|
|
1212
|
+
*/
|
|
1213
|
+
warnedOpenInviteWhitelist = false;
|
|
1205
1214
|
constructor(opts, users, auth, consentStore) {
|
|
1206
1215
|
this.opts = mergeAuthWorkflowOpts(opts);
|
|
1207
1216
|
this.users = users;
|
|
@@ -1281,12 +1290,56 @@ let AuthWorkflow = class AuthWorkflow {
|
|
|
1281
1290
|
*/
|
|
1282
1291
|
afterMfaChanged(_ctx) {}
|
|
1283
1292
|
/**
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
* `
|
|
1287
|
-
*
|
|
1293
|
+
* Selectable role ids for the admin invite form — ALSO enforced server-side
|
|
1294
|
+
* (`admin-form` rejects any submitted role outside this set). Read by
|
|
1295
|
+
* `prepareAvailableRoles`. Mirrors the prior `InviteWorkflow.getAvailableRoles()`
|
|
1296
|
+
* consumer hook.
|
|
1297
|
+
*
|
|
1298
|
+
* Default behaviour is driven by {@link AuthWorkflowOpts.invitableRoles}:
|
|
1299
|
+
* - **configured** → that universe intersected with the CURRENT inviter's
|
|
1300
|
+
* ARBAC grants (`auth.invite` / `assign:<role>`), so an inviter can only
|
|
1301
|
+
* delegate roles they may themselves assign. If ARBAC is unreachable the
|
|
1302
|
+
* universe is returned verbatim — still a CLOSED whitelist, never fail-open.
|
|
1303
|
+
* - **unset** → `undefined`, preserving the legacy "no whitelist" behaviour;
|
|
1304
|
+
* `prepareAvailableRoles` warns once unless
|
|
1305
|
+
* {@link AuthWorkflowOpts.allowAnyInviteRole} acknowledges it.
|
|
1306
|
+
*
|
|
1307
|
+
* Override for fully custom sourcing — the override then owns the gate
|
|
1308
|
+
* outright (no warning fires; `invitableRoles` is consulted only if the
|
|
1309
|
+
* override reads it).
|
|
1310
|
+
*/
|
|
1311
|
+
getAvailableRoles() {
|
|
1312
|
+
const universe = this.opts.invitableRoles;
|
|
1313
|
+
if (universe.length === 0) return void 0;
|
|
1314
|
+
return this.filterInvitableRolesByArbac(universe);
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Intersect a role universe with the current inviter's ARBAC grants — keep
|
|
1318
|
+
* only roles they hold `auth.invite` / `assign:<role>` for. Falls back to the
|
|
1319
|
+
* universe verbatim when ARBAC is unreachable (no event context / not wired),
|
|
1320
|
+
* which keeps the whitelist CLOSED rather than failing open.
|
|
1288
1321
|
*/
|
|
1289
|
-
|
|
1322
|
+
async filterInvitableRolesByArbac(universe) {
|
|
1323
|
+
try {
|
|
1324
|
+
const arbac = useArbac();
|
|
1325
|
+
const verdicts = await Promise.all(universe.map((role) => arbac.evaluate({
|
|
1326
|
+
resource: "auth.invite",
|
|
1327
|
+
action: `assign:${role}`
|
|
1328
|
+
})));
|
|
1329
|
+
return universe.filter((_, i) => verdicts[i].allowed);
|
|
1330
|
+
} catch {
|
|
1331
|
+
return [...universe];
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* True when the admin invite form would assign roles with NO server-side
|
|
1336
|
+
* whitelist in effect: `invitableRoles` unset, `getAvailableRoles` not
|
|
1337
|
+
* overridden, and the open default not acknowledged via `allowAnyInviteRole`.
|
|
1338
|
+
* Drives the one-time `prepare-available-roles` warning.
|
|
1339
|
+
*/
|
|
1340
|
+
inviteWhitelistIsOpen() {
|
|
1341
|
+
return !this.opts.allowAnyInviteRole && this.opts.invitableRoles.length === 0 && this.getAvailableRoles === _AuthWorkflow.prototype.getAvailableRoles;
|
|
1342
|
+
}
|
|
1290
1343
|
/**
|
|
1291
1344
|
* Build the extras dict merged into the freshly-created user row. Runs for
|
|
1292
1345
|
* EVERY new-account path: password-signup and invite-accept merge it at
|
|
@@ -1728,6 +1781,33 @@ let AuthWorkflow = class AuthWorkflow {
|
|
|
1728
1781
|
};
|
|
1729
1782
|
}
|
|
1730
1783
|
/**
|
|
1784
|
+
* Copy (heading + intro) for the unified set-password screen, staged by
|
|
1785
|
+
* `create-password-form` BEFORE the pause. Branches on the resolved
|
|
1786
|
+
* `ctx.password.changeReason` (`expired` / `reset`), then invite-accept
|
|
1787
|
+
* (`ctx.accept`), then the initial-password fallback. Override to re-brand any
|
|
1788
|
+
* phase; return a partial (`{ heading }` only) to change one field and leave
|
|
1789
|
+
* the other at whatever an earlier step staged, or `{}` to keep both as-is.
|
|
1790
|
+
*/
|
|
1791
|
+
resolveSetPasswordCopy(ctx) {
|
|
1792
|
+
const reason = ctx.password?.changeReason;
|
|
1793
|
+
if (reason === "expired") return {
|
|
1794
|
+
heading: "Your password has expired",
|
|
1795
|
+
intro: "Choose a new password to continue. The previous one is no longer valid."
|
|
1796
|
+
};
|
|
1797
|
+
if (reason === "reset") return {
|
|
1798
|
+
heading: "Reset your password",
|
|
1799
|
+
intro: "Choose a new password for your account."
|
|
1800
|
+
};
|
|
1801
|
+
if (ctx.accept) return {
|
|
1802
|
+
heading: "Welcome — set your password",
|
|
1803
|
+
intro: "Choose a password to activate your account."
|
|
1804
|
+
};
|
|
1805
|
+
return {
|
|
1806
|
+
heading: "Set your initial password",
|
|
1807
|
+
intro: "Your account was created without a password. Choose one to continue."
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1731
1811
|
* Resolve the recovery post-reset policy. Reached from recovery.flow.
|
|
1732
1812
|
* `freshLoginRequired` REMOVED — the auto-login choice is the static
|
|
1733
1813
|
* `AuthWorkflowOpts.autoLoginOnRecover` boolean (per §2 decision).
|
|
@@ -2704,6 +2784,10 @@ let AuthWorkflow = class AuthWorkflow {
|
|
|
2704
2784
|
async prepareAvailableRoles(ctx) {
|
|
2705
2785
|
const roles = await this.getAvailableRoles();
|
|
2706
2786
|
if (roles) (ctx.admin ??= {}).availableRoles = roles;
|
|
2787
|
+
else if (!this.warnedOpenInviteWhitelist && this.inviteWhitelistIsOpen()) {
|
|
2788
|
+
this.warnedOpenInviteWhitelist = true;
|
|
2789
|
+
useLogger("aooth:auth", current()).warn("Invite role whitelist is OFF — the admin invite form can assign ANY role, including privileged ones. Set `invitableRoles` (intersected with the inviter's ARBAC grants), override `getAvailableRoles()`, or set `allowAnyInviteRole: true` to acknowledge the open default.");
|
|
2790
|
+
}
|
|
2707
2791
|
}
|
|
2708
2792
|
/**
|
|
2709
2793
|
* Merges policy from `resolveAccept` into `ctx.accept` (rather than
|
|
@@ -2736,16 +2820,33 @@ let AuthWorkflow = class AuthWorkflow {
|
|
|
2736
2820
|
ctx.recoveryAltActions = result;
|
|
2737
2821
|
}
|
|
2738
2822
|
/**
|
|
2823
|
+
* Roles the server will honor from the admin invite form. SECURITY boundary:
|
|
2824
|
+
* when `resolveAdminForm` returned `collectRoles: false` the role picker is
|
|
2825
|
+
* hidden, so any submitted `roles[]` is a crafted or buggy payload and is
|
|
2826
|
+
* IGNORED — roles in that mode come from `inferAdminRoles` (server-side),
|
|
2827
|
+
* never client input. This keeps role authorization independent of whether
|
|
2828
|
+
* `prepare-available-roles` ran: that step populates the `availableRoles`
|
|
2829
|
+
* whitelist the `admin-form` guard enforces against, but it is schema-gated on
|
|
2830
|
+
* `collectRoles`, so WITHOUT this check a `collectRoles:false` deployment
|
|
2831
|
+
* would let a crafted POST assign ANY role with no whitelist / per-role ARBAC
|
|
2832
|
+
* `assign:<role>` check.
|
|
2833
|
+
*/
|
|
2834
|
+
effectiveInviteRoles(ctx, submitted) {
|
|
2835
|
+
if (ctx.adminForm?.collectRoles === false) return [];
|
|
2836
|
+
return parseInviteRoles(submitted);
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2739
2839
|
* Admin-side invite form. Pauses for `InviteForm`; binds `ctx.email` +
|
|
2740
2840
|
* `ctx.admin.roles`. Server-side enforces the `availableRoles` whitelist
|
|
2741
|
-
* (populated by `prepare-available-roles`)
|
|
2742
|
-
*
|
|
2841
|
+
* (populated by `prepare-available-roles`) and, via {@link effectiveInviteRoles},
|
|
2842
|
+
* ignores any submitted roles when `resolveAdminForm` set `collectRoles:false`.
|
|
2843
|
+
* Calls `duplicateInviteCheck` to decide whether to reject duplicates.
|
|
2743
2844
|
*/
|
|
2744
2845
|
async adminForm(ctx) {
|
|
2745
2846
|
const wf = this.useAtscriptWfPublic(ctx, this.opts.forms.invite);
|
|
2746
2847
|
const input = wf.resolveInput();
|
|
2747
2848
|
const email = input.email;
|
|
2748
|
-
const parsed =
|
|
2849
|
+
const parsed = this.effectiveInviteRoles(ctx, input.roles);
|
|
2749
2850
|
if (Array.isArray(ctx.admin?.availableRoles)) {
|
|
2750
2851
|
const allowed = new Set(ctx.admin.availableRoles);
|
|
2751
2852
|
if (parsed.find((r) => !allowed.has(r)) !== void 0) throw this.throwPublic(ctx, wf, { errors: { roles: "Invalid role" } });
|
|
@@ -2926,19 +3027,9 @@ let AuthWorkflow = class AuthWorkflow {
|
|
|
2926
3027
|
async createPasswordForm(ctx) {
|
|
2927
3028
|
this.requireSubject(ctx);
|
|
2928
3029
|
const password = ctx.password ??= {};
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
} else if (password.changeReason === "reset") {
|
|
2933
|
-
password.heading = "Reset your password";
|
|
2934
|
-
password.intro = "Choose a new password for your account.";
|
|
2935
|
-
} else if (ctx.accept) {
|
|
2936
|
-
password.heading = "Welcome — set your password";
|
|
2937
|
-
password.intro = "Choose a password to activate your account.";
|
|
2938
|
-
} else {
|
|
2939
|
-
password.heading = "Set your initial password";
|
|
2940
|
-
password.intro = "Your account was created without a password. Choose one to continue.";
|
|
2941
|
-
}
|
|
3030
|
+
const copy = await this.resolveSetPasswordCopy(ctx);
|
|
3031
|
+
if (copy.heading !== void 0) password.heading = copy.heading;
|
|
3032
|
+
if (copy.intro !== void 0) password.intro = copy.intro;
|
|
2942
3033
|
const wf = this.useAtscriptWfPublic(ctx, this.opts.forms.setPassword);
|
|
2943
3034
|
let input;
|
|
2944
3035
|
try {
|
|
@@ -6124,7 +6215,7 @@ __decorate([
|
|
|
6124
6215
|
__decorateMetadata("design:paramtypes", []),
|
|
6125
6216
|
__decorateMetadata("design:returntype", void 0)
|
|
6126
6217
|
], AuthWorkflow.prototype, "signupFlow", null);
|
|
6127
|
-
AuthWorkflow = __decorate([
|
|
6218
|
+
AuthWorkflow = _AuthWorkflow = __decorate([
|
|
6128
6219
|
Inherit(),
|
|
6129
6220
|
Controller(),
|
|
6130
6221
|
__decorateMetadata("design:paramtypes", [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/auth-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Moost auth integration for aoothjs — AuthGuard interceptor, useAuth composable, REST endpoints, workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aoothjs",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@atscript/moost-wf": "^0.1.107",
|
|
61
61
|
"@wooksjs/http-body": "^0.7.19",
|
|
62
|
-
"@aooth/arbac-moost": "^0.1.
|
|
63
|
-
"@aooth/idp": "0.1.
|
|
64
|
-
"@aooth/auth": "0.1.
|
|
65
|
-
"@aooth/user": "0.1.
|
|
62
|
+
"@aooth/arbac-moost": "^0.1.31",
|
|
63
|
+
"@aooth/idp": "0.1.31",
|
|
64
|
+
"@aooth/auth": "0.1.31",
|
|
65
|
+
"@aooth/user": "0.1.31"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@atscript/core": "^0.1.78",
|