@voltro/plugin-auth 0.86.0 → 0.87.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 +5321 -0
- package/README.md +83 -0
- package/THIRD-PARTY-NOTICES.md +1130 -11
- package/dist/authExtensions.js +9 -0
- package/dist/emailVerification.js +13 -11
- package/dist/guest.d.ts +99 -0
- package/dist/guest.js +3 -0
- package/dist/guestClient.js +237 -0
- package/dist/guestCredential.js +12 -0
- package/dist/guestPasswordUpgrade.js +85 -0
- package/dist/guestPlugin.js +166 -0
- package/dist/guestSchema.js +20 -0
- package/dist/guestSecrets.js +10 -0
- package/dist/guestService.js +224 -0
- package/dist/handlers.js +113 -113
- package/dist/impersonation.js +79 -78
- package/dist/index.d.ts +265 -15
- package/dist/index.js +27 -19
- package/dist/invitations.js +23 -23
- package/dist/native/guestStorage.js +41 -0
- package/dist/native.d.ts +110 -0
- package/dist/native.js +45 -43
- package/dist/nativeRoutes.js +61 -72
- package/dist/overlayPlugin.js +167 -0
- package/dist/plugin.d.ts +119 -10
- package/dist/plugin.js +99 -78
- package/dist/postgresUserStore.js +19 -7
- package/dist/revocation.js +40 -23
- package/dist/schema.d.ts +41 -1
- package/dist/schema.js +99 -98
- package/dist/sessionFamily.js +15 -0
- package/dist/sessionRegistry.js +93 -78
- package/dist/signedClaims.js +24 -0
- package/dist/strategy.d.ts +13 -9
- package/dist/strategy.js +26 -22
- package/dist/userStore.js +145 -138
- package/dist/web/guestStorage.js +47 -0
- package/dist/web.d.ts +94 -0
- package/dist/web.js +4 -2
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { AuthStrategy } from '@voltro/protocol';
|
|
2
|
+
import { ColumnBuilder } from '@voltro/database';
|
|
2
3
|
import { ConnectionCredential } from '@voltro/protocol';
|
|
3
4
|
import { DataStore } from '@voltro/database';
|
|
5
|
+
import { Duration } from 'effect';
|
|
4
6
|
import { Effect } from 'effect';
|
|
7
|
+
import { FieldDefinitions } from '@voltro/database';
|
|
5
8
|
import { KeyedSecret } from '@voltro/protocol/session';
|
|
6
9
|
import { MembershipsSource } from '@voltro/protocol';
|
|
7
10
|
import { SessionSecrets } from '@voltro/protocol/session';
|
|
8
11
|
import { SqlClient } from 'effect/sql/SqlClient';
|
|
9
12
|
import { Subject } from '@voltro/protocol';
|
|
10
13
|
import { SubjectIdentity } from '@voltro/protocol';
|
|
14
|
+
import { Table } from '@voltro/database';
|
|
11
15
|
import { VerifyOptions } from '@voltro/protocol/session';
|
|
12
16
|
import { VerifyResult } from '@voltro/protocol/session';
|
|
13
17
|
import { VoidIfEmpty } from 'effect/Types';
|
|
@@ -42,6 +46,9 @@ export declare interface AdmissionRefusal {
|
|
|
42
46
|
/** Decide whether a person may join a tenant — `null` to let them in. */
|
|
43
47
|
export declare type AdmitJoining = (joining: Joining, context: AdmissionContext) => Effect.Effect<AdmissionRefusal | null> | Promise<AdmissionRefusal | null>;
|
|
44
48
|
|
|
49
|
+
/** The page a ticket may open: same-origin, below an allowed path, no traversal. */
|
|
50
|
+
export declare const allowedOverlayPage: (next: string, paths: ReadonlyArray<string>) => boolean;
|
|
51
|
+
|
|
45
52
|
/**
|
|
46
53
|
* Keep users in the app's own database.
|
|
47
54
|
*
|
|
@@ -83,6 +90,16 @@ export declare interface AssertionVerifyInput {
|
|
|
83
90
|
readonly storedCounter: number;
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
/** General authentication extensions; install `auth.guest(...)` or `auth.overlay(...)` in plugins. */
|
|
94
|
+
export declare const auth: {
|
|
95
|
+
readonly guest: (options: GuestPluginOptions) => GuestAuthPlugin;
|
|
96
|
+
readonly overlay: (options: OverlayPluginOptions) => VoltroPlugin;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Non-session handoffs have separate signature domains. A signed OAuth
|
|
100
|
+
* cookie must never become a native login code or a registered session. */
|
|
101
|
+
export declare type AuthClaimsPurpose = 'native-pending' | 'native-code' | 'social-login' | 'social-upgrade' | 'social-native-upgrade' | 'account-merge';
|
|
102
|
+
|
|
86
103
|
export declare interface AuthConfig {
|
|
87
104
|
/**
|
|
88
105
|
* The session-signing key. OMIT IT and the value is read from
|
|
@@ -175,6 +192,27 @@ export declare interface AuthConfig {
|
|
|
175
192
|
readonly emailVerification?: EmailVerificationConfig;
|
|
176
193
|
}
|
|
177
194
|
|
|
195
|
+
/** Device credentials never appear in SQL, logs, exports or session claims. */
|
|
196
|
+
export declare const authGuestDevicesTable: Table<"authGuestDevices", FieldDefinitions<{
|
|
197
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
198
|
+
readonly guestId: ColumnBuilder<string, "text", boolean>;
|
|
199
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
200
|
+
readonly secretHash: ColumnBuilder<string, "text", boolean>;
|
|
201
|
+
readonly generation: ColumnBuilder<number, "integer", boolean>;
|
|
202
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
203
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
204
|
+
}>, true, never>;
|
|
205
|
+
|
|
206
|
+
/** Guest identity is independent of the password user's email record. */
|
|
207
|
+
export declare const authGuestsTable: Table<"authGuests", FieldDefinitions<{
|
|
208
|
+
readonly id: ColumnBuilder<string, "id", boolean>;
|
|
209
|
+
readonly tenantId: ColumnBuilder<string, "text", boolean>;
|
|
210
|
+
readonly upgradeable: ColumnBuilder<boolean, "boolean", boolean>;
|
|
211
|
+
readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
212
|
+
readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
213
|
+
readonly lastSeenAt: ColumnBuilder<Date, "timestamp", boolean>;
|
|
214
|
+
}>, true, never>;
|
|
215
|
+
|
|
178
216
|
export declare const authMemberships: (store: UserStore) => MembershipsSource;
|
|
179
217
|
|
|
180
218
|
/**
|
|
@@ -183,7 +221,7 @@ export declare const authMemberships: (store: UserStore) => MembershipsSource;
|
|
|
183
221
|
* routes use) so the framework's serve pipeline can slot it into the
|
|
184
222
|
* per-app auth chain — the rpc/WS path then rejects revoked sessions
|
|
185
223
|
* exactly like the `/auth/*` routes. Read it via
|
|
186
|
-
* `
|
|
224
|
+
* `getAuthSessionStrategies`.
|
|
187
225
|
*/
|
|
188
226
|
export declare interface AuthPlugin extends VoltroPlugin {
|
|
189
227
|
readonly auth: {
|
|
@@ -233,6 +271,15 @@ export declare interface AuthRoutesPluginOptions extends AuthConfig {
|
|
|
233
271
|
* otherwise get 500s from a route it never asked for.
|
|
234
272
|
*/
|
|
235
273
|
readonly invitations?: InvitationsConfig;
|
|
274
|
+
/**
|
|
275
|
+
* Guest → account with email and password. PRESENT mounts `POST /auth/upgrade-guest`
|
|
276
|
+
* (`{ credential, email, password }`, CSRF): the guest becomes a registered account under its
|
|
277
|
+
* own subject ID, so what it owns stays its own. Pass the installed guest lifecycle port,
|
|
278
|
+
* e.g. `{ service: () => guestPlugin.guest.service() }`. ABSENT mounts nothing.
|
|
279
|
+
*/
|
|
280
|
+
readonly guestUpgrade?: {
|
|
281
|
+
readonly service: () => GuestAuthService;
|
|
282
|
+
};
|
|
236
283
|
/**
|
|
237
284
|
* User impersonation ("log in as"). PRESENT mounts `/auth/impersonate/start`
|
|
238
285
|
* + `/stop`, arms the retention bound on `impersonationGrants`, and closes
|
|
@@ -267,6 +314,10 @@ export declare interface ChallengeStore {
|
|
|
267
314
|
*/
|
|
268
315
|
export declare const clearSessionCookie: (options?: IssueSessionOptions) => string;
|
|
269
316
|
|
|
317
|
+
/** Upgrade the presented guest to an email/password account under the same subject ID. An email
|
|
318
|
+
* that already has an account refuses (`email_taken`); this never merges accounts. */
|
|
319
|
+
export declare const completeGuestPasswordUpgrade: (options: GuestPasswordUpgradeOptions) => Effect.Effect<GuestPasswordUpgradeResult>;
|
|
320
|
+
|
|
270
321
|
/** The keyed secret set a handler signs/verifies with — the explicit
|
|
271
322
|
* `config.secrets` when set, else `config.secret` widened with the
|
|
272
323
|
* rotation env vars. */
|
|
@@ -304,6 +355,10 @@ export declare const CSRF_HEADER_NAME: "x-csrf-token";
|
|
|
304
355
|
*/
|
|
305
356
|
export declare const dataStoreChallengeStore: (sql: SqlClient) => ChallengeStore;
|
|
306
357
|
|
|
358
|
+
/** Uses the app's existing DataStore and session issuer. Device proof grants
|
|
359
|
+
* guest identity only; provider linking is a separate trusted-account operation. */
|
|
360
|
+
export declare const dataStoreGuestAuth: (store: DataStore, options: GuestAuthOptions) => GuestAuthService;
|
|
361
|
+
|
|
307
362
|
/**
|
|
308
363
|
* Session rows in the app's own database — the `sessions` table from
|
|
309
364
|
* `@voltro/plugin-auth/schema`, which the app registers like any other. Reads
|
|
@@ -450,11 +505,12 @@ export declare const generateTotpCode: (secretBase32: string, nowMs?: number) =>
|
|
|
450
505
|
export declare const generateTotpSecret: () => string;
|
|
451
506
|
|
|
452
507
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
508
|
+
* Collect pre-wired session strategies in plugin declaration order. The
|
|
509
|
+
* framework inserts each original strategy into its shared auth chain, so
|
|
510
|
+
* prefix ordering, collision diagnostics and boot verification see them all.
|
|
511
|
+
* Only `skip` advances; rejected credentials never get another attempt.
|
|
456
512
|
*/
|
|
457
|
-
export declare const
|
|
513
|
+
export declare const getAuthSessionStrategies: (plugins: ReadonlyArray<VoltroPlugin>) => ReadonlyArray<AuthStrategy>;
|
|
458
514
|
|
|
459
515
|
export declare interface GrantRoleContext {
|
|
460
516
|
/** The inviter's role in THIS tenant, as read from `memberships`. */
|
|
@@ -477,6 +533,149 @@ export declare interface GrantRoleContext {
|
|
|
477
533
|
*/
|
|
478
534
|
export declare const guardSubject: (user: UserRecord, config: AuthConfig) => Effect.Effect<HandlerResult | null>;
|
|
479
535
|
|
|
536
|
+
export declare class GuestAuthError extends GuestAuthError_base<{
|
|
537
|
+
readonly code: 'credential_rejected' | 'store_unavailable' | 'upgrade_conflict';
|
|
538
|
+
}> {
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
declare const GuestAuthError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
|
|
542
|
+
readonly _tag: "GuestAuthError";
|
|
543
|
+
} & Readonly<A>;
|
|
544
|
+
|
|
545
|
+
export declare interface GuestAuthOptions {
|
|
546
|
+
/** Server-owned tenant selection; never copied from a device or request body. */
|
|
547
|
+
readonly tenantId: string;
|
|
548
|
+
readonly secret: SessionSecretInput | (() => SessionSecretInput);
|
|
549
|
+
readonly upgradeable?: boolean;
|
|
550
|
+
readonly ttlSeconds?: number;
|
|
551
|
+
readonly sessionTtlSeconds?: number;
|
|
552
|
+
readonly secure?: boolean;
|
|
553
|
+
readonly now?: () => number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export declare interface GuestAuthPlugin extends AuthPlugin {
|
|
557
|
+
readonly guest: {
|
|
558
|
+
/** Server-side lifecycle/privacy port, available after normal store binding. */
|
|
559
|
+
readonly service: () => GuestAuthService;
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export declare const guestAuthPlugin: (options: GuestPluginOptions) => GuestAuthPlugin;
|
|
564
|
+
|
|
565
|
+
/** Durable guest lifecycle port. Implementations must serialize every write
|
|
566
|
+
* against the guest identity and commit session rows before returning credentials. */
|
|
567
|
+
export declare interface GuestAuthService {
|
|
568
|
+
readonly create: () => Effect.Effect<GuestSession, GuestAuthError>;
|
|
569
|
+
readonly resume: (credential: string) => Effect.Effect<GuestSession, GuestAuthError>;
|
|
570
|
+
/** Propose a fresh server-minted proof without replacing the active proof. */
|
|
571
|
+
readonly prepareRotation: (credential: string) => Effect.Effect<{
|
|
572
|
+
readonly deviceCredential: string;
|
|
573
|
+
}, GuestAuthError>;
|
|
574
|
+
/** The caller durably journals both proofs before this atomic replacement. */
|
|
575
|
+
readonly rotate: (credential: string, nextCredential: string) => Effect.Effect<GuestSession, GuestAuthError>;
|
|
576
|
+
readonly revoke: (credential: string) => Effect.Effect<void, GuestAuthError>;
|
|
577
|
+
/** Server-side privacy hook: the caller must authorize this subject ID. */
|
|
578
|
+
readonly erase: (subjectId: string) => Effect.Effect<boolean, GuestAuthError>;
|
|
579
|
+
/** Server-side privacy hook: excludes credential hashes and credential bytes. */
|
|
580
|
+
readonly export: (subjectId: string) => Effect.Effect<GuestExport | null, GuestAuthError>;
|
|
581
|
+
readonly isLive: (subject: SubjectIdentity) => Effect.Effect<boolean, GuestAuthError>;
|
|
582
|
+
/** Trusted server operation. The writer must use this transaction to create
|
|
583
|
+
* the registered `users` row with exactly the guest ID and tenant. The guest
|
|
584
|
+
* fence, writer, device removal and guest-session revocation commit together.
|
|
585
|
+
* No client-supplied kind/state can authorize an upgrade. */
|
|
586
|
+
readonly upgrade: <A>(credential: string, writer: GuestUpgradeWriter<A>) => Effect.Effect<A, GuestAuthError>;
|
|
587
|
+
/** Trusted, separately confirmed merge into an EXISTING registered subject.
|
|
588
|
+
* The coordinator must prove target ownership and run all data hooks through
|
|
589
|
+
* this same transaction. A device proof alone never proves target ownership. */
|
|
590
|
+
readonly mergeInto: <A>(credential: string, targetId: string, writer: GuestMergeWriter<A>) => Effect.Effect<A, GuestAuthError>;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export declare interface GuestExport {
|
|
594
|
+
readonly subject: {
|
|
595
|
+
readonly id: string;
|
|
596
|
+
readonly tenantId: string;
|
|
597
|
+
readonly kind: 'guest';
|
|
598
|
+
readonly upgradeable: boolean;
|
|
599
|
+
readonly createdAt: number;
|
|
600
|
+
readonly expiresAt: number;
|
|
601
|
+
};
|
|
602
|
+
readonly devices: ReadonlyArray<{
|
|
603
|
+
readonly id: string;
|
|
604
|
+
readonly generation: number;
|
|
605
|
+
readonly createdAt: number;
|
|
606
|
+
readonly expiresAt: number;
|
|
607
|
+
}>;
|
|
608
|
+
readonly sessions: ReadonlyArray<{
|
|
609
|
+
readonly id: string;
|
|
610
|
+
readonly expiresAt: number;
|
|
611
|
+
}>;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export declare type GuestIdentity = Extract<SubjectIdentity, {
|
|
615
|
+
readonly type: 'user';
|
|
616
|
+
}> & {
|
|
617
|
+
readonly kind: 'guest';
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export declare interface GuestMergeContext extends GuestUpgradeContext {
|
|
621
|
+
readonly targetId: string;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export declare type GuestMergeWriter<A> = (context: GuestMergeContext) => Promise<A>;
|
|
625
|
+
|
|
626
|
+
export declare interface GuestPasswordUpgradeOptions {
|
|
627
|
+
/** The guest's device proof, as the guest client presents it. */
|
|
628
|
+
readonly credential: string;
|
|
629
|
+
readonly email: string;
|
|
630
|
+
readonly password: string;
|
|
631
|
+
/** The installed guest lifecycle port (`guestPlugin.guest.service()`). */
|
|
632
|
+
readonly service: GuestAuthService;
|
|
633
|
+
readonly users: UserStore;
|
|
634
|
+
readonly config: AuthConfig;
|
|
635
|
+
readonly ipAddress?: string | null;
|
|
636
|
+
readonly userAgent?: string | null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export declare type GuestPasswordUpgradeResult = {
|
|
640
|
+
readonly kind: 'ok';
|
|
641
|
+
readonly userId: string;
|
|
642
|
+
readonly subject: unknown;
|
|
643
|
+
readonly setCookie: string;
|
|
644
|
+
} | {
|
|
645
|
+
readonly kind: 'refused';
|
|
646
|
+
readonly status: number;
|
|
647
|
+
readonly code: string;
|
|
648
|
+
readonly message: string;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
export declare interface GuestPluginOptions {
|
|
652
|
+
/** Server-owned tenant, required; client headers and device proof cannot pick it. */
|
|
653
|
+
readonly tenantId: string;
|
|
654
|
+
readonly upgradeable?: boolean;
|
|
655
|
+
readonly ttl?: Duration.Input;
|
|
656
|
+
readonly sessionTtl?: Duration.Input;
|
|
657
|
+
readonly secret?: SessionSecretInput;
|
|
658
|
+
readonly secure?: boolean;
|
|
659
|
+
readonly prefix?: string;
|
|
660
|
+
/** Inject a complete durable lifecycle port; never an ephemeral credential fallback. */
|
|
661
|
+
readonly service?: (store: DataStore) => GuestAuthService;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export declare interface GuestSession extends IssuedRegisteredSession {
|
|
665
|
+
readonly subject: GuestIdentity;
|
|
666
|
+
/** Opaque proof returned only on creation or explicit credential rotation. */
|
|
667
|
+
readonly deviceCredential?: string;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export declare interface GuestUpgradeContext {
|
|
671
|
+
readonly store: DataStore;
|
|
672
|
+
readonly subjectId: string;
|
|
673
|
+
readonly tenantId: string;
|
|
674
|
+
readonly createdAt: Date;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export declare type GuestUpgradeWriter<A> = (context: GuestUpgradeContext) => Promise<A>;
|
|
678
|
+
|
|
480
679
|
/**
|
|
481
680
|
* Accept an invitation as an ALREADY-AUTHENTICATED user.
|
|
482
681
|
*
|
|
@@ -1370,6 +1569,13 @@ export declare interface LockoutStore {
|
|
|
1370
1569
|
readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
|
|
1371
1570
|
}
|
|
1372
1571
|
|
|
1572
|
+
/** Read an owned, unrotated credential under its canonical family fence.
|
|
1573
|
+
* Call only inside the caller's existing transaction, after any application
|
|
1574
|
+
* account/tenant fences. Locks remain held until that transaction ends. This
|
|
1575
|
+
* checks persistence identity, not the signed credential, expiry or permissions.
|
|
1576
|
+
* Locking-read conflicts follow the selected store's transaction semantics. */
|
|
1577
|
+
export declare const lockRegisteredSession: (store: DataStore, userId: string, sessionId: string) => Promise<SessionRecord | null>;
|
|
1578
|
+
|
|
1373
1579
|
/** Default magic-link lifetime — short, since it's an inbox round-trip. */
|
|
1374
1580
|
export declare const MAGIC_LINK_TTL_S: number;
|
|
1375
1581
|
|
|
@@ -1611,6 +1817,32 @@ export declare const otpauthUrl: (params: {
|
|
|
1611
1817
|
readonly secret: string;
|
|
1612
1818
|
}) => string;
|
|
1613
1819
|
|
|
1820
|
+
/**
|
|
1821
|
+
* Mount the web overlay handoff: an engine client trades its header session
|
|
1822
|
+
* for a one-time URL that signs an embedded browser in to `paths` of the web
|
|
1823
|
+
* app with the same session row. Install as `auth.overlay({ paths: ['/overlay'] })`.
|
|
1824
|
+
*/
|
|
1825
|
+
export declare const overlayAuthPlugin: (options: OverlayPluginOptions) => VoltroPlugin;
|
|
1826
|
+
|
|
1827
|
+
/** Options of `auth.overlay(...)`, the web overlay handoff for engine clients. */
|
|
1828
|
+
export declare interface OverlayPluginOptions {
|
|
1829
|
+
/** Pages a ticket may open, e.g. `['/overlay']`. A page is allowed when its
|
|
1830
|
+
* path equals one of these or lies below it. Required: the handoff signs
|
|
1831
|
+
* the embedded browser in, so it lands only where the app expects it. */
|
|
1832
|
+
readonly paths: ReadonlyArray<string>;
|
|
1833
|
+
/** Lifetime of a ticket. Default 60 seconds, at most 5 minutes. */
|
|
1834
|
+
readonly ttl?: Duration.Input;
|
|
1835
|
+
/** Signing keys of the sessions this handoff accepts. Default: the
|
|
1836
|
+
* project's `VOLTRO_SESSION_SECRET` (and `_PREVIOUS`). */
|
|
1837
|
+
readonly secret?: SessionSecretInput;
|
|
1838
|
+
/** `Secure` on the cookie. Default `true`; `false` only for plain-http development. */
|
|
1839
|
+
readonly secure?: boolean;
|
|
1840
|
+
/** Cookie `Domain`, when the web app and the API share a parent domain. */
|
|
1841
|
+
readonly domain?: string;
|
|
1842
|
+
/** Route prefix. Default `/auth/overlay`. */
|
|
1843
|
+
readonly prefix?: string;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1614
1846
|
declare interface ParsedHash {
|
|
1615
1847
|
readonly N: number;
|
|
1616
1848
|
readonly r: number;
|
|
@@ -1618,6 +1850,13 @@ declare interface ParsedHash {
|
|
|
1618
1850
|
readonly keyLen: number;
|
|
1619
1851
|
}
|
|
1620
1852
|
|
|
1853
|
+
/** Browser-safe shape only. Possession is verified against SQL on the server. */
|
|
1854
|
+
export declare const parseGuestCredential: (credential: string) => {
|
|
1855
|
+
readonly guestId: string;
|
|
1856
|
+
readonly deviceId: string;
|
|
1857
|
+
readonly secret: string;
|
|
1858
|
+
} | null;
|
|
1859
|
+
|
|
1621
1860
|
/** Parse the scrypt parameters out of a stored hash. Returns null when the
|
|
1622
1861
|
* string isn't a well-formed `scrypt$N$r$p$salt$derived`, or when its
|
|
1623
1862
|
* parameters exceed the derivation ceiling. */
|
|
@@ -1913,15 +2152,18 @@ export declare interface SessionRecord {
|
|
|
1913
2152
|
export declare type SessionRegistry = SessionStore;
|
|
1914
2153
|
|
|
1915
2154
|
export declare interface SessionRegistryStoreOptions {
|
|
1916
|
-
/** Borrow an already owned transaction; otherwise registry writes open one.
|
|
2155
|
+
/** Borrow an already owned transaction; otherwise registry writes open one.
|
|
2156
|
+
* Account-wide revocation needs the application's real owner fence. With an
|
|
2157
|
+
* older repeatable-read snapshot, root issuance must also change that owner's
|
|
2158
|
+
* persisted revision so a conflicting old transaction retries or aborts. */
|
|
1917
2159
|
readonly transaction?: 'existing';
|
|
1918
2160
|
}
|
|
1919
2161
|
|
|
1920
2162
|
export declare interface SessionRevocationChecker {
|
|
1921
|
-
/** Is this session
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
readonly
|
|
2163
|
+
/** Is this exact session credential live for the signed subject's user ID?
|
|
2164
|
+
* Refuse a missing/rotated/expired row or a different stored owner. Cache
|
|
2165
|
+
* verdicts retain native owner identity, never the requested owner. */
|
|
2166
|
+
readonly isLiveFor: (sessionId: string, userId: string) => Effect.Effect<boolean>;
|
|
1925
2167
|
/** Drop the cached verdict for one session — called inline by
|
|
1926
2168
|
* sign-out / revoke handlers so the kill is immediate on THIS
|
|
1927
2169
|
* process (other replicas converge within `ttlMs`). */
|
|
@@ -1967,13 +2209,14 @@ export declare const sessionSecretsOf: (secret: SessionSecretInput) => SessionSe
|
|
|
1967
2209
|
|
|
1968
2210
|
/** Server-side sessions — enumeration + revocation. */
|
|
1969
2211
|
export declare interface SessionStore {
|
|
1970
|
-
/**
|
|
2212
|
+
/** Unrotated credentials with an existing canonical root owned by the same user, most recent first. */
|
|
1971
2213
|
readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
|
|
1972
2214
|
/** Record a session on sign-in. */
|
|
1973
2215
|
readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
|
|
1974
|
-
/** Look up
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
2216
|
+
/** Look up an unrotated credential only while its canonical family root
|
|
2217
|
+
* exists and belongs to the same user. A rotated root remains an anchor;
|
|
2218
|
+
* its tenant does not replace the credential tenant. Custom stores must
|
|
2219
|
+
* enforce this before returning a row to request-time revocation checks. */
|
|
1977
2220
|
readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
|
|
1978
2221
|
/** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
|
|
1979
2222
|
* when the sliding-window renewal re-issues the cookie, so the row
|
|
@@ -1992,6 +2235,8 @@ export declare interface SessionStore {
|
|
|
1992
2235
|
readonly revokeAllSessions: (userId: string) => Effect.Effect<number>;
|
|
1993
2236
|
}
|
|
1994
2237
|
|
|
2238
|
+
export declare const signAuthClaims: (claims: Readonly<Record<string, unknown>>, secret: string, purpose: AuthClaimsPurpose) => string;
|
|
2239
|
+
|
|
1995
2240
|
export declare interface SignInInput {
|
|
1996
2241
|
readonly email: string;
|
|
1997
2242
|
readonly password: string;
|
|
@@ -2171,7 +2416,7 @@ declare const UserNotFoundError_base: new <A extends Record<string, any> = {}>(a
|
|
|
2171
2416
|
|
|
2172
2417
|
export declare interface UserRecord {
|
|
2173
2418
|
readonly id: string;
|
|
2174
|
-
readonly email: string;
|
|
2419
|
+
readonly email: string | null;
|
|
2175
2420
|
/**
|
|
2176
2421
|
* The stored scrypt hash — OPTIONAL, because not every identity has a
|
|
2177
2422
|
* password. An SSO-only, magic-link-only, passkey-only, or provider-PAT
|
|
@@ -2236,6 +2481,8 @@ export declare type UserStoreResolver = (ctx: UserStoreBindContext) => UserStore
|
|
|
2236
2481
|
/** Verify a passkey assertion (sign-in). */
|
|
2237
2482
|
export declare const verifyAssertion: (input: AssertionVerifyInput) => AssertionOutcome;
|
|
2238
2483
|
|
|
2484
|
+
export declare const verifyAuthClaims: (value: string | undefined, secret: string, purpose: AuthClaimsPurpose, now?: () => number) => Record<string, unknown> | null;
|
|
2485
|
+
|
|
2239
2486
|
/**
|
|
2240
2487
|
* Verify a state-changing request's CSRF protection. Both the header
|
|
2241
2488
|
* value and the cookie value must (a) be the SAME token and (b) carry a
|
|
@@ -2321,6 +2568,9 @@ export declare interface VoltroPasswordStrategyOptions {
|
|
|
2321
2568
|
* (stateless); with it, nothing but a registered session authenticates,
|
|
2322
2569
|
* and every session can be revoked. */
|
|
2323
2570
|
readonly requireSessionId?: boolean;
|
|
2571
|
+
/** Verified user identity kinds owned by this carrier. Other verified kinds
|
|
2572
|
+
* skip to their own carrier; malformed, forged or expired values still fail. */
|
|
2573
|
+
readonly userKinds?: ReadonlyArray<'registered' | 'guest'>;
|
|
2324
2574
|
}
|
|
2325
2575
|
|
|
2326
2576
|
export declare type WebAuthnError = 'bad-client-data' | 'wrong-type' | 'challenge-mismatch' | 'origin-mismatch' | 'rpid-mismatch' | 'user-not-present' | 'bad-auth-data' | 'no-credential' | 'unsupported-key' | 'bad-signature' | 'counter-replay';
|
package/dist/index.js
CHANGED
|
@@ -2,22 +2,30 @@ import { PasswordEmptyError as e, PasswordHashError as t, hashPassword as n, nee
|
|
|
2
2
|
import { SESSION_COOKIE_NAME as s, clearSessionCookie as c, issueSession as l, readSession as u, readSessionFromHeaders as d, readSessionKeyed as f, resolveSessionSecret as p, resolveSessionSecrets as m, sessionSecretsOf as h } from "./session.js";
|
|
3
3
|
import { makeSessionRevocationChecker as g } from "./revocation.js";
|
|
4
4
|
import { UserAlreadyExistsError as _, UserNotFoundError as v, memoryUserStore as y, newAuthId as b, sessionIdOfSubject as x, subjectFromUser as S, subjectMemberships as C } from "./userStore.js";
|
|
5
|
-
import { dataStoreSessionRegistry as w, issueRegisteredSession as T,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
5
|
+
import { dataStoreSessionRegistry as w, issueRegisteredSession as T, lockRegisteredSession as E, rotateRegisteredSession as D, withSessionId as O } from "./sessionRegistry.js";
|
|
6
|
+
import { CSRF_COOKIE_NAME as k, CSRF_HEADER_NAME as A, isCsrfTokenWellFormed as j, issueCsrfToken as M, verifyCsrf as N } from "./csrf.js";
|
|
7
|
+
import { parseGuestCredential as P } from "./guestCredential.js";
|
|
8
|
+
import { GuestAuthError as F, dataStoreGuestAuth as I } from "./guestService.js";
|
|
9
|
+
import { authGuestDevicesTable as L, authGuestsTable as R } from "./guestSchema.js";
|
|
10
|
+
import { guestAuthPlugin as z } from "./guestPlugin.js";
|
|
11
|
+
import { allowedOverlayPage as B, overlayAuthPlugin as V } from "./overlayPlugin.js";
|
|
12
|
+
import { auth as H } from "./authExtensions.js";
|
|
13
|
+
import { TotpVerifyError as U, generateRecoveryCodes as W, generateTotpCode as G, generateTotpSecret as K, otpauthUrl as q, verifyTotpCode as J } from "./totp.js";
|
|
14
|
+
import { EMAIL_VERIFY_TTL_S as Y, MAGIC_LINK_TTL_S as X, MFA_PENDING_TTL_S as Z, NATIVE_CODE_TTL_S as Q, PASSWORD_RESET_TTL_S as $, hashToken as ee, mintSecret as te, mintToken as ne } from "./tokens.js";
|
|
15
|
+
import { runSubjectGuards as re } from "./subjectGuard.js";
|
|
16
|
+
import { EMAIL_NOT_VERIFIED_CODE as ie, EMAIL_VERIFIED_METADATA_KEY as ae, EMAIL_VERIFY_RESEND_COOLDOWN_S as oe, emailVerificationActive as se, emailVerificationGuard as ce, handleEmailVerifyConfirm as le, handleEmailVerifyRequest as ue, isEmailVerified as de, sendEmailVerification as fe, userEmailVerified as pe } from "./emailVerification.js";
|
|
17
|
+
import { configSessionSecrets as me, guardSubject as he, handleCsrf as ge, handleListMemberships as _e, handleListSessions as ve, handleMagicLinkConsume as ye, handleMagicLinkRequest as be, handleMfaEnrollStart as xe, handleMfaEnrollVerify as Se, handleMfaRegenerateRecoveryCodes as Ce, handleMfaUnenroll as we, handleMfaVerify as Te, handlePasswordResetConfirm as Ee, handlePasswordResetRequest as De, handleRevokeAllOtherSessions as Oe, handleRevokeSession as ke, handleSignIn as Ae, handleSignOut as je, handleSignUp as Me, handleSwitchTenant as Ne, issueUserSession as Pe } from "./handlers.js";
|
|
18
|
+
import { completeGuestPasswordUpgrade as Fe } from "./guestPasswordUpgrade.js";
|
|
19
|
+
import { authMemberships as Ie } from "./memberships.js";
|
|
20
|
+
import { postgresUserStore as Le } from "./postgresUserStore.js";
|
|
21
|
+
import { appUserStore as Re } from "./appUserStore.js";
|
|
22
|
+
import { generateChallenge as ze, verifyAssertion as Be, verifyRegistration as Ve } from "./webauthn.js";
|
|
23
|
+
import { dataStoreChallengeStore as He, handlePasskeyAssertOptions as Ue, handlePasskeyAssertVerify as We, handlePasskeyRegisterOptions as Ge, handlePasskeyRegisterVerify as Ke, memoryChallengeStore as qe } from "./passkeyHandlers.js";
|
|
24
|
+
import { DEFAULT_INVITER_ROLES as Je, DEFAULT_ROLE_RANK as Ye, INVITATION_MAX_PENDING as Xe, INVITATION_TTL_S as Ze, defaultCanGrantRole as Qe, handleAcceptInvitation as $e, handleAcceptInvitationWithSignUp as et, handleCreateInvitation as tt, handleListInvitations as nt, handlePreviewInvitation as rt, handleRevokeInvitation as it } from "./invitations.js";
|
|
25
|
+
import { IMPERSONATION_DEFAULT_DURATION_S as at, IMPERSONATION_FORBIDDEN_SUBPATHS as ot, handleImpersonationStart as st, handleImpersonationStop as ct, impersonationAuditRedactor as lt, impersonationOf as ut, isImpersonated as dt } from "./impersonation.js";
|
|
26
|
+
import { signAuthClaims as ft, verifyAuthClaims as pt } from "./signedClaims.js";
|
|
27
|
+
import { NATIVE_PENDING_COOKIE as mt, handleNativeAuthorize as ht, handleNativeComplete as gt, handleNativeExchange as _t, isAllowedRedirectUri as vt, nativeLoginRedirect as yt } from "./nativeRoutes.js";
|
|
28
|
+
import { voltroPasswordStrategy as bt } from "./strategy.js";
|
|
29
|
+
import { authRoutesPlugin as xt, getAuthSessionStrategies as St } from "./plugin.js";
|
|
30
|
+
import { mailSender as Ct } from "./mail.js";
|
|
31
|
+
export { k as CSRF_COOKIE_NAME, A as CSRF_HEADER_NAME, Je as DEFAULT_INVITER_ROLES, Ye as DEFAULT_ROLE_RANK, ie as EMAIL_NOT_VERIFIED_CODE, ae as EMAIL_VERIFIED_METADATA_KEY, oe as EMAIL_VERIFY_RESEND_COOLDOWN_S, Y as EMAIL_VERIFY_TTL_S, F as GuestAuthError, at as IMPERSONATION_DEFAULT_DURATION_S, ot as IMPERSONATION_FORBIDDEN_SUBPATHS, Xe as INVITATION_MAX_PENDING, Ze as INVITATION_TTL_S, X as MAGIC_LINK_TTL_S, Z as MFA_PENDING_TTL_S, Q as NATIVE_CODE_TTL_S, mt as NATIVE_PENDING_COOKIE, $ as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, s as SESSION_COOKIE_NAME, U as TotpVerifyError, _ as UserAlreadyExistsError, v as UserNotFoundError, B as allowedOverlayPage, Re as appUserStore, H as auth, L as authGuestDevicesTable, R as authGuestsTable, Ie as authMemberships, xt as authRoutesPlugin, c as clearSessionCookie, Fe as completeGuestPasswordUpgrade, me as configSessionSecrets, He as dataStoreChallengeStore, I as dataStoreGuestAuth, w as dataStoreSessionRegistry, Qe as defaultCanGrantRole, se as emailVerificationActive, ce as emailVerificationGuard, ze as generateChallenge, W as generateRecoveryCodes, G as generateTotpCode, K as generateTotpSecret, St as getAuthSessionStrategies, he as guardSubject, z as guestAuthPlugin, $e as handleAcceptInvitation, et as handleAcceptInvitationWithSignUp, tt as handleCreateInvitation, ge as handleCsrf, le as handleEmailVerifyConfirm, ue as handleEmailVerifyRequest, st as handleImpersonationStart, ct as handleImpersonationStop, nt as handleListInvitations, _e as handleListMemberships, ve as handleListSessions, ye as handleMagicLinkConsume, be as handleMagicLinkRequest, xe as handleMfaEnrollStart, Se as handleMfaEnrollVerify, Ce as handleMfaRegenerateRecoveryCodes, we as handleMfaUnenroll, Te as handleMfaVerify, ht as handleNativeAuthorize, gt as handleNativeComplete, _t as handleNativeExchange, Ue as handlePasskeyAssertOptions, We as handlePasskeyAssertVerify, Ge as handlePasskeyRegisterOptions, Ke as handlePasskeyRegisterVerify, Ee as handlePasswordResetConfirm, De as handlePasswordResetRequest, rt as handlePreviewInvitation, Oe as handleRevokeAllOtherSessions, it as handleRevokeInvitation, ke as handleRevokeSession, Ae as handleSignIn, je as handleSignOut, Me as handleSignUp, Ne as handleSwitchTenant, n as hashPassword, ee as hashToken, lt as impersonationAuditRedactor, ut as impersonationOf, vt as isAllowedRedirectUri, j as isCsrfTokenWellFormed, de as isEmailVerified, dt as isImpersonated, M as issueCsrfToken, T as issueRegisteredSession, l as issueSession, Pe as issueUserSession, E as lockRegisteredSession, Ct as mailSender, g as makeSessionRevocationChecker, qe as memoryChallengeStore, y as memoryUserStore, te as mintSecret, ne as mintToken, yt as nativeLoginRedirect, r as needsRehash, b as newAuthId, q as otpauthUrl, V as overlayAuthPlugin, P as parseGuestCredential, i as parseScryptParams, Le as postgresUserStore, u as readSession, d as readSessionFromHeaders, f as readSessionKeyed, p as resolveSessionSecret, m as resolveSessionSecrets, D as rotateRegisteredSession, re as runSubjectGuards, fe as sendEmailVerification, x as sessionIdOfSubject, h as sessionSecretsOf, ft as signAuthClaims, S as subjectFromUser, C as subjectMemberships, pe as userEmailVerified, Be as verifyAssertion, pt as verifyAuthClaims, N as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, Ve as verifyRegistration, J as verifyTotpCode, bt as voltroPasswordStrategy, O as withSessionId };
|
package/dist/invitations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { newAuthId as e } from "./userStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { decideAdmission as t } from "./admission.js";
|
|
3
|
+
import { hashToken as n, mintSecret as r } from "./tokens.js";
|
|
4
4
|
import { Effect as i } from "effect";
|
|
5
5
|
//#region src/invitations.ts
|
|
6
6
|
var a = 604800, o = 500, s = [
|
|
@@ -34,12 +34,12 @@ var a = 604800, o = 500, s = [
|
|
|
34
34
|
}), h = (e, t, n, r) => i.gen(function* () {
|
|
35
35
|
let i = yield* n.membershipRole(e, t);
|
|
36
36
|
return i === null || !(r?.inviterRoles ?? c).includes(i) ? f(403, { error: "not_permitted" }) : { role: i };
|
|
37
|
-
}), g = (e) => "status" in e, _ = (t,
|
|
37
|
+
}), g = (e) => "status" in e, _ = (t, n, a) => i.gen(function* () {
|
|
38
38
|
let o = t.email.trim().toLowerCase();
|
|
39
39
|
if (!o || !o.includes("@")) return f(400, { error: "a valid email is required" });
|
|
40
40
|
if (!t.tenantId) return f(400, { error: "tenantId required" });
|
|
41
41
|
if (!t.role) return f(400, { error: "role required" });
|
|
42
|
-
let s = a.invitations, c = yield* h(t.inviterUserId, t.tenantId,
|
|
42
|
+
let s = a.invitations, c = yield* h(t.inviterUserId, t.tenantId, n, s);
|
|
43
43
|
if (g(c)) return c;
|
|
44
44
|
if (!d(s)({
|
|
45
45
|
inviterRole: c.role,
|
|
@@ -50,9 +50,9 @@ var a = 604800, o = 500, s = [
|
|
|
50
50
|
message: `A '${c.role}' may not invite a '${t.role}'.`
|
|
51
51
|
});
|
|
52
52
|
let l = new Date((a.now ?? Date.now)());
|
|
53
|
-
if ((yield*
|
|
54
|
-
yield*
|
|
55
|
-
let u = s?.ttlSeconds ?? 604800, _ =
|
|
53
|
+
if ((yield* n.countPendingInvitations(t.tenantId, l)) >= (s?.maxPending ?? 500)) return f(429, { error: "too_many_pending_invitations" });
|
|
54
|
+
yield* n.supersedePendingInvitations(t.tenantId, o, t.inviterUserId, l).pipe(i.ignore);
|
|
55
|
+
let u = s?.ttlSeconds ?? 604800, _ = r(u), v = yield* n.insertInvitation({
|
|
56
56
|
id: e("inv"),
|
|
57
57
|
tokenHash: _.tokenHash,
|
|
58
58
|
email: o,
|
|
@@ -88,9 +88,9 @@ var a = 604800, o = 500, s = [
|
|
|
88
88
|
ok: !0,
|
|
89
89
|
revoked: 1
|
|
90
90
|
}) : f(404, { error: "invitation_not_found" });
|
|
91
|
-
}), b = (e,
|
|
91
|
+
}), b = (e, t, r = {}) => i.gen(function* () {
|
|
92
92
|
if (!e.token) return f(400, { error: "token required" });
|
|
93
|
-
let i = yield*
|
|
93
|
+
let i = yield* t.findInvitationByHash(n(e.token));
|
|
94
94
|
if (!i) return f(404, { error: "invalid_invitation" });
|
|
95
95
|
let a = new Date((r.now ?? Date.now)());
|
|
96
96
|
return f(200, {
|
|
@@ -101,24 +101,24 @@ var a = 604800, o = 500, s = [
|
|
|
101
101
|
role: i.role,
|
|
102
102
|
expiresAt: i.expiresAt.toISOString()
|
|
103
103
|
});
|
|
104
|
-
}), x = (e,
|
|
104
|
+
}), x = (e, r, a = {}) => i.gen(function* () {
|
|
105
105
|
if (!e.token) return f(400, { error: "token required" });
|
|
106
|
-
let o =
|
|
106
|
+
let o = n(e.token), s = new Date((a.now ?? Date.now)()), c = yield* r.findInvitationByHash(o);
|
|
107
107
|
if (!c) return f(401, { error: "invalid_or_expired_invitation" });
|
|
108
108
|
if (!e.userId) return f(401, {
|
|
109
109
|
error: "authentication_required",
|
|
110
110
|
email: c.email
|
|
111
111
|
});
|
|
112
|
-
let l = yield*
|
|
112
|
+
let l = yield* r.findById(e.userId);
|
|
113
113
|
if (!l) return f(401, {
|
|
114
114
|
error: "authentication_required",
|
|
115
115
|
email: c.email
|
|
116
116
|
});
|
|
117
|
-
if (l.email.toLowerCase() !== c.email.toLowerCase()) return f(403, {
|
|
117
|
+
if (l.email === null || l.email.toLowerCase() !== c.email.toLowerCase()) return f(403, {
|
|
118
118
|
error: "invitation_email_mismatch",
|
|
119
119
|
message: "This invitation was issued to a different email address. Sign in as that address to accept it."
|
|
120
120
|
});
|
|
121
|
-
let u = yield*
|
|
121
|
+
let u = yield* t(a.admit, {
|
|
122
122
|
email: c.email,
|
|
123
123
|
tenantId: c.tenantId,
|
|
124
124
|
via: "invitation"
|
|
@@ -128,14 +128,14 @@ var a = 604800, o = 500, s = [
|
|
|
128
128
|
reason: u.reason,
|
|
129
129
|
message: u.message
|
|
130
130
|
});
|
|
131
|
-
let d = yield*
|
|
131
|
+
let d = yield* r.acceptInvitation(o, l.id, s);
|
|
132
132
|
if (!d) return f(401, { error: "invalid_or_expired_invitation" });
|
|
133
|
-
let p = yield*
|
|
133
|
+
let p = yield* r.addMembership({
|
|
134
134
|
userId: l.id,
|
|
135
135
|
tenantId: d.tenantId,
|
|
136
136
|
role: d.role
|
|
137
137
|
});
|
|
138
|
-
return yield*
|
|
138
|
+
return yield* r.markEmailVerified(l.id).pipe(i.ignore), f(200, {
|
|
139
139
|
ok: !0,
|
|
140
140
|
membership: {
|
|
141
141
|
tenantId: p.tenantId,
|
|
@@ -143,18 +143,18 @@ var a = 604800, o = 500, s = [
|
|
|
143
143
|
joinedAt: p.joinedAt.toISOString()
|
|
144
144
|
}
|
|
145
145
|
});
|
|
146
|
-
}), S = (
|
|
147
|
-
if (!
|
|
148
|
-
if (
|
|
149
|
-
let s =
|
|
146
|
+
}), S = (r, a, o) => i.gen(function* () {
|
|
147
|
+
if (!r.token || !r.password) return f(400, { error: "token and password required" });
|
|
148
|
+
if (r.password.length < (o.minPasswordLength ?? 8)) return f(400, { error: "password must be at least 8 characters" });
|
|
149
|
+
let s = n(r.token), c = new Date((o.now ?? Date.now)()), l = yield* a.findInvitationByHash(s);
|
|
150
150
|
if (!l) return f(401, { error: "invalid_or_expired_invitation" });
|
|
151
151
|
if (yield* a.findByEmail(l.email)) return f(409, {
|
|
152
152
|
error: "account_exists",
|
|
153
153
|
message: "An account already exists for this address. Sign in, then accept the invitation."
|
|
154
154
|
});
|
|
155
|
-
let u = yield* o.hashPassword(
|
|
155
|
+
let u = yield* o.hashPassword(r.password).pipe(i.orElseSucceed(() => ""));
|
|
156
156
|
if (!u) return f(500, { error: "hash_failed" });
|
|
157
|
-
let d = yield*
|
|
157
|
+
let d = yield* t(o.admit, {
|
|
158
158
|
email: l.email,
|
|
159
159
|
tenantId: l.tenantId,
|
|
160
160
|
via: "invitation"
|