@voltro/plugin-auth 0.33.0 → 0.34.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 +1801 -0
- package/dist/index.d.ts +830 -26
- package/dist/index.js +182 -52
- package/dist/password.d.ts +2 -1
- package/dist/password.js +44 -35
- package/dist/plugin-BZm5Semm.js +1330 -0
- package/dist/plugin.d.ts +315 -44
- package/dist/plugin.js +2 -1
- package/dist/schema.d.ts +46 -1
- package/dist/schema.js +33 -3
- package/dist/session.d.ts +13 -5
- package/dist/{strategy-Ce4FlGi4.js → strategy-Bty6_XUj.js} +84 -12
- package/dist/strategy.d.ts +111 -1
- package/dist/strategy.js +1 -1
- package/dist/tokens.d.ts +36 -1
- package/dist/tokens.js +15 -8
- package/package.json +3 -3
- package/dist/plugin-ZXs8LFn0.js +0 -820
|
@@ -35,9 +35,9 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
35
35
|
kind: "typeid",
|
|
36
36
|
prefix: e
|
|
37
37
|
}, e), p = (e = []) => {
|
|
38
|
-
let t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), i = [], a = [], o = [], s = [], c = [], l = /* @__PURE__ */ new Map();
|
|
38
|
+
let t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), i = [], a = [], o = [], s = [], c = [], l = [], p = [], m = /* @__PURE__ */ new Map();
|
|
39
39
|
for (let n of e) t.set(n.id, n), r.set(n.email.toLowerCase(), n);
|
|
40
|
-
let
|
|
40
|
+
let h = (e, i) => n.gen(function* () {
|
|
41
41
|
let a = t.get(e);
|
|
42
42
|
if (!a) return yield* n.fail(new d({ userId: e }));
|
|
43
43
|
let o = i(a);
|
|
@@ -56,24 +56,28 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
56
56
|
};
|
|
57
57
|
return t.set(a.id, a), r.set(i, a), a;
|
|
58
58
|
}),
|
|
59
|
-
updatePassword: (e, t) =>
|
|
59
|
+
updatePassword: (e, t) => h(e, (e) => ({
|
|
60
60
|
...e,
|
|
61
61
|
passwordHash: t
|
|
62
62
|
})),
|
|
63
|
-
setMfaSecret: (e, t) =>
|
|
63
|
+
setMfaSecret: (e, t) => h(e, (e) => ({
|
|
64
64
|
...e,
|
|
65
65
|
mfaSecret: t,
|
|
66
66
|
mfaEnrolledAt: null
|
|
67
67
|
})),
|
|
68
|
-
markMfaEnrolled: (e) =>
|
|
68
|
+
markMfaEnrolled: (e) => h(e, (e) => ({
|
|
69
69
|
...e,
|
|
70
70
|
mfaEnrolledAt: /* @__PURE__ */ new Date()
|
|
71
71
|
})),
|
|
72
|
-
clearMfa: (e) =>
|
|
72
|
+
clearMfa: (e) => h(e, (e) => ({
|
|
73
73
|
...e,
|
|
74
74
|
mfaSecret: null,
|
|
75
75
|
mfaEnrolledAt: null
|
|
76
76
|
})),
|
|
77
|
+
markEmailVerified: (e) => h(e, (e) => e.emailVerifiedAt ? e : {
|
|
78
|
+
...e,
|
|
79
|
+
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
80
|
+
}),
|
|
77
81
|
listMemberships: (e) => n.sync(() => i.filter((t) => t.userId === e)),
|
|
78
82
|
addMembership: (e) => n.sync(() => {
|
|
79
83
|
let t = i.findIndex((t) => t.userId === e.userId && t.tenantId === e.tenantId), n = {
|
|
@@ -137,6 +141,74 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
137
141
|
}, a = o.indexOf(r);
|
|
138
142
|
return o[a] = i, i;
|
|
139
143
|
}),
|
|
144
|
+
latestToken: (e, t) => n.sync(() => {
|
|
145
|
+
let n = null;
|
|
146
|
+
for (let r of o) r.userId !== e || r.purpose !== t || (n === null || r.createdAt.getTime() > n.createdAt.getTime()) && (n = r);
|
|
147
|
+
return n;
|
|
148
|
+
}),
|
|
149
|
+
insertInvitation: (e) => n.sync(() => {
|
|
150
|
+
let t = {
|
|
151
|
+
...e,
|
|
152
|
+
email: e.email.toLowerCase(),
|
|
153
|
+
acceptedAt: null,
|
|
154
|
+
acceptedByUserId: null,
|
|
155
|
+
revokedAt: null,
|
|
156
|
+
revokedByUserId: null,
|
|
157
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
158
|
+
};
|
|
159
|
+
return l.push(t), t;
|
|
160
|
+
}),
|
|
161
|
+
findInvitationByHash: (e) => n.sync(() => l.find((t) => t.tokenHash === e) ?? null),
|
|
162
|
+
acceptInvitation: (e, t, r) => n.sync(() => {
|
|
163
|
+
let n = l.findIndex((t) => t.tokenHash === e && t.acceptedAt === null && t.revokedAt === null && t.expiresAt.getTime() > r.getTime());
|
|
164
|
+
if (n < 0) return null;
|
|
165
|
+
let i = {
|
|
166
|
+
...l[n],
|
|
167
|
+
acceptedAt: r,
|
|
168
|
+
acceptedByUserId: t
|
|
169
|
+
};
|
|
170
|
+
return l[n] = i, i;
|
|
171
|
+
}),
|
|
172
|
+
revokeInvitation: (e, t, r, i) => n.sync(() => {
|
|
173
|
+
let n = l.findIndex((n) => n.id === e && n.tenantId === t && n.acceptedAt === null && n.revokedAt === null);
|
|
174
|
+
return n < 0 ? !1 : (l[n] = {
|
|
175
|
+
...l[n],
|
|
176
|
+
revokedAt: i,
|
|
177
|
+
revokedByUserId: r
|
|
178
|
+
}, !0);
|
|
179
|
+
}),
|
|
180
|
+
listInvitations: (e) => n.sync(() => l.filter((t) => t.tenantId === e).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
|
|
181
|
+
countPendingInvitations: (e, t) => n.sync(() => l.filter((n) => n.tenantId === e && n.acceptedAt === null && n.revokedAt === null && n.expiresAt.getTime() > t.getTime()).length),
|
|
182
|
+
supersedePendingInvitations: (e, t, r, i) => n.sync(() => {
|
|
183
|
+
let n = t.toLowerCase(), a = 0;
|
|
184
|
+
for (let t = 0; t < l.length; t++) {
|
|
185
|
+
let o = l[t];
|
|
186
|
+
o.tenantId === e && o.email === n && o.acceptedAt === null && o.revokedAt === null && (l[t] = {
|
|
187
|
+
...o,
|
|
188
|
+
revokedAt: i,
|
|
189
|
+
revokedByUserId: r
|
|
190
|
+
}, a++);
|
|
191
|
+
}
|
|
192
|
+
return a;
|
|
193
|
+
}),
|
|
194
|
+
insertImpersonationGrant: (e) => n.sync(() => {
|
|
195
|
+
let t = {
|
|
196
|
+
...e,
|
|
197
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
198
|
+
endedAt: null,
|
|
199
|
+
endedReason: null
|
|
200
|
+
};
|
|
201
|
+
return p.push(t), t;
|
|
202
|
+
}),
|
|
203
|
+
findImpersonationGrantBySession: (e) => n.sync(() => p.find((t) => t.sessionId === e) ?? null),
|
|
204
|
+
endImpersonationGrant: (e, t, r) => n.sync(() => {
|
|
205
|
+
let n = p.findIndex((t) => t.sessionId === e && t.endedAt === null);
|
|
206
|
+
return n < 0 ? !1 : (p[n] = {
|
|
207
|
+
...p[n],
|
|
208
|
+
endedAt: r,
|
|
209
|
+
endedReason: t
|
|
210
|
+
}, !0);
|
|
211
|
+
}),
|
|
140
212
|
replaceRecoveryCodes: (e, t) => n.sync(() => {
|
|
141
213
|
for (let t = c.length - 1; t >= 0; t--) c[t].userId === e && c.splice(t, 1);
|
|
142
214
|
let n = /* @__PURE__ */ new Date();
|
|
@@ -175,7 +247,7 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
175
247
|
}, !0);
|
|
176
248
|
}),
|
|
177
249
|
isLockedOut: (e, t) => n.sync(() => {
|
|
178
|
-
let n =
|
|
250
|
+
let n = m.get(e.toLowerCase());
|
|
179
251
|
return n?.lockedUntil != null && t < n.lockedUntil ? {
|
|
180
252
|
locked: !0,
|
|
181
253
|
retryAfterSeconds: Math.ceil((n.lockedUntil - t) / 1e3)
|
|
@@ -185,15 +257,15 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
185
257
|
};
|
|
186
258
|
}),
|
|
187
259
|
recordLoginFailure: (e, t, r) => n.sync(() => {
|
|
188
|
-
let n = e.toLowerCase(), i =
|
|
189
|
-
|
|
260
|
+
let n = e.toLowerCase(), i = m.get(n), a = i?.windowStartedAt == null || t - i.windowStartedAt >= r.windowMs, o = i?.lockedUntil != null && t >= i.lockedUntil, s = !i || a || o, c = s ? 1 : i.failedCount + 1, l = s ? t : i.windowStartedAt, u = c >= r.maxAttempts ? t + r.lockMs : null;
|
|
261
|
+
m.set(n, {
|
|
190
262
|
failedCount: c,
|
|
191
|
-
windowStartedAt:
|
|
192
|
-
lockedUntil:
|
|
263
|
+
windowStartedAt: l,
|
|
264
|
+
lockedUntil: u
|
|
193
265
|
});
|
|
194
266
|
}),
|
|
195
267
|
clearLoginFailures: (e) => n.sync(() => {
|
|
196
|
-
|
|
268
|
+
m.delete(e.toLowerCase());
|
|
197
269
|
})
|
|
198
270
|
};
|
|
199
271
|
}, m = (e, t = {}) => {
|
package/dist/strategy.d.ts
CHANGED
|
@@ -4,6 +4,49 @@ import { SessionSecrets } from '@voltro/protocol/session';
|
|
|
4
4
|
import { VoidIfEmpty } from 'effect/Types';
|
|
5
5
|
import { YieldableError } from 'effect/Cause';
|
|
6
6
|
|
|
7
|
+
/** One impersonation grant — "actor is acting as target, on this session,
|
|
8
|
+
* until this instant". See `impersonationGrantsTable`. */
|
|
9
|
+
declare interface ImpersonationGrantRecord {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
/** The IMPERSONATED session's row id. */
|
|
12
|
+
readonly sessionId: string;
|
|
13
|
+
readonly actorUserId: string;
|
|
14
|
+
/** The impersonator's own session, restored when the grant ends. */
|
|
15
|
+
readonly actorSessionId: string;
|
|
16
|
+
readonly targetUserId: string;
|
|
17
|
+
readonly tenantId: string;
|
|
18
|
+
readonly reason: string | null;
|
|
19
|
+
readonly startedAt: Date;
|
|
20
|
+
readonly expiresAt: Date;
|
|
21
|
+
readonly endedAt: Date | null;
|
|
22
|
+
readonly endedReason: string | null;
|
|
23
|
+
readonly ipAddress?: string | null;
|
|
24
|
+
readonly userAgent?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A tenant invitation — the grant that turns into a `memberships` row when
|
|
29
|
+
* redeemed. See `invitationsTable` for why `role` lives here and not in the
|
|
30
|
+
* accept request.
|
|
31
|
+
*/
|
|
32
|
+
declare interface InvitationRecord {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
/** SHA-256 hex of the plaintext token. The plaintext lives only in the email. */
|
|
35
|
+
readonly tokenHash: string;
|
|
36
|
+
/** The invited address, lower-cased. */
|
|
37
|
+
readonly email: string;
|
|
38
|
+
readonly tenantId: string;
|
|
39
|
+
/** The role the membership is created with. Chosen by the INVITER. */
|
|
40
|
+
readonly role: string;
|
|
41
|
+
readonly invitedByUserId: string;
|
|
42
|
+
readonly expiresAt: Date;
|
|
43
|
+
readonly acceptedAt: Date | null;
|
|
44
|
+
readonly acceptedByUserId: string | null;
|
|
45
|
+
readonly revokedAt: Date | null;
|
|
46
|
+
readonly revokedByUserId: string | null;
|
|
47
|
+
readonly createdAt: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
7
50
|
/**
|
|
8
51
|
* Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
|
|
9
52
|
* INTO the store on each failure so the store itself stays policy-free.
|
|
@@ -90,7 +133,7 @@ declare interface SessionRevocationOptions {
|
|
|
90
133
|
* or the keyed `{ current, previous? }` rotation set. */
|
|
91
134
|
declare type SessionSecretInput = string | SessionSecrets;
|
|
92
135
|
|
|
93
|
-
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending';
|
|
136
|
+
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify';
|
|
94
137
|
|
|
95
138
|
/** A single-use, hashed, expiring token (magic-link / password-reset). */
|
|
96
139
|
declare interface TokenRecord {
|
|
@@ -150,6 +193,18 @@ declare interface UserRecord {
|
|
|
150
193
|
* verify). Distinct from the secret being set — a half-completed
|
|
151
194
|
* enrolment leaves `mfaSecret` populated but this null. */
|
|
152
195
|
readonly mfaEnrolledAt?: Date | null;
|
|
196
|
+
/**
|
|
197
|
+
* When this address was PROVEN to reach its owner — by redeeming a
|
|
198
|
+
* verification link, a magic link, or a password-reset link. `null` /
|
|
199
|
+
* absent = unproven.
|
|
200
|
+
*
|
|
201
|
+
* A store whose backing table predates the `emailVerifiedAt` column simply
|
|
202
|
+
* leaves it undefined, which reads as unverified — and that is why
|
|
203
|
+
* `emailVerification.policy` defaults to `'off'`: under any stricter
|
|
204
|
+
* default, "the column is not there yet" and "this user never confirmed"
|
|
205
|
+
* would both refuse the login.
|
|
206
|
+
*/
|
|
207
|
+
readonly emailVerifiedAt?: Date | null;
|
|
153
208
|
/** Optional lifecycle flag read by post-authentication subject guards.
|
|
154
209
|
* Set (a `Date`) means the account is DEACTIVATED — `@voltro/plugin-
|
|
155
210
|
* deactivation`'s `deactivationGuard()` vetoes login when it's non-null.
|
|
@@ -178,6 +233,15 @@ declare interface UserStore {
|
|
|
178
233
|
/** Wipe the secret + enrolledAt. Used by /account/security's
|
|
179
234
|
* "remove MFA" action. */
|
|
180
235
|
readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
236
|
+
/**
|
|
237
|
+
* Stamp `emailVerifiedAt` — the address has been proven.
|
|
238
|
+
*
|
|
239
|
+
* IDEMPOTENT and non-regressing: a user already verified keeps their
|
|
240
|
+
* ORIGINAL timestamp. "When was this address first proven" is the question
|
|
241
|
+
* the column answers, and re-stamping it on every later magic-link login
|
|
242
|
+
* would quietly turn it into "when did they last click something".
|
|
243
|
+
*/
|
|
244
|
+
readonly markEmailVerified: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
|
|
181
245
|
/** Every tenant the user belongs to. */
|
|
182
246
|
readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
|
|
183
247
|
/** Idempotent grant: inserts the [userId, tenantId] membership or
|
|
@@ -215,6 +279,52 @@ declare interface UserStore {
|
|
|
215
279
|
* already consumed — and marks it consumed in the same step. Returns
|
|
216
280
|
* null otherwise (single-use guard). */
|
|
217
281
|
readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
|
|
282
|
+
/**
|
|
283
|
+
* The most recently CREATED token of this purpose for the user, consumed or
|
|
284
|
+
* not — or null when there is none.
|
|
285
|
+
*
|
|
286
|
+
* Exists for the resend cooldown, which is the only bound on an endpoint
|
|
287
|
+
* that sends mail to an address supplied by an unauthenticated caller. Note
|
|
288
|
+
* it deliberately does NOT filter on `consumedAt`: the cost being rate-
|
|
289
|
+
* limited is the EMAIL, and a token that was minted and then redeemed cost
|
|
290
|
+
* exactly as much to send as one still pending.
|
|
291
|
+
*/
|
|
292
|
+
readonly latestToken: (userId: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
|
|
293
|
+
/** Persist an invitation (token already hashed). */
|
|
294
|
+
readonly insertInvitation: (invitation: Omit<InvitationRecord, 'acceptedAt' | 'acceptedByUserId' | 'revokedAt' | 'revokedByUserId' | 'createdAt'>) => Effect.Effect<InvitationRecord>;
|
|
295
|
+
/** Look an invitation up by token hash WITHOUT redeeming it — the read the
|
|
296
|
+
* "what am I being invited to" preview needs. Returns the row whatever its
|
|
297
|
+
* state; the caller decides what an expired or revoked one means. */
|
|
298
|
+
readonly findInvitationByHash: (tokenHash: string) => Effect.Effect<InvitationRecord | null>;
|
|
299
|
+
/**
|
|
300
|
+
* Atomically redeem an invitation: returns the row only when it exists, is
|
|
301
|
+
* unaccepted, unrevoked and unexpired — and stamps `acceptedAt` +
|
|
302
|
+
* `acceptedByUserId` in the same step. Null otherwise.
|
|
303
|
+
*
|
|
304
|
+
* The single-use guard, and it has to be ONE statement: two clicks on the
|
|
305
|
+
* same emailed link race, and a read-then-write would let both through and
|
|
306
|
+
* write the membership twice.
|
|
307
|
+
*/
|
|
308
|
+
readonly acceptInvitation: (tokenHash: string, acceptedByUserId: string, now: Date) => Effect.Effect<InvitationRecord | null>;
|
|
309
|
+
/** Withdraw a PENDING invitation, scoped to its tenant so an admin of one
|
|
310
|
+
* tenant cannot revoke another's by id. Returns whether a row changed. */
|
|
311
|
+
readonly revokeInvitation: (invitationId: string, tenantId: string, revokedByUserId: string, now: Date) => Effect.Effect<boolean>;
|
|
312
|
+
/** Every invitation for a tenant, newest first. The admin list. */
|
|
313
|
+
readonly listInvitations: (tenantId: string) => Effect.Effect<ReadonlyArray<InvitationRecord>>;
|
|
314
|
+
/** How many invitations for this tenant are still redeemable — the bound
|
|
315
|
+
* `maxPending` enforces. */
|
|
316
|
+
readonly countPendingInvitations: (tenantId: string, now: Date) => Effect.Effect<number>;
|
|
317
|
+
/** Revoke every pending invitation for (tenant, email). Called before
|
|
318
|
+
* issuing a fresh one so re-inviting supersedes rather than accumulates —
|
|
319
|
+
* otherwise every resend leaves a live token behind. Returns the count. */
|
|
320
|
+
readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
|
|
321
|
+
/** Record an impersonation grant when the impersonated session is minted. */
|
|
322
|
+
readonly insertImpersonationGrant: (grant: Omit<ImpersonationGrantRecord, 'startedAt' | 'endedAt' | 'endedReason'>) => Effect.Effect<ImpersonationGrantRecord>;
|
|
323
|
+
/** The grant for an impersonated session, or null. The stop path's lookup. */
|
|
324
|
+
readonly findImpersonationGrantBySession: (sessionId: string) => Effect.Effect<ImpersonationGrantRecord | null>;
|
|
325
|
+
/** Close a grant. Only an OPEN grant is closed (`endedAt IS NULL`), so a
|
|
326
|
+
* double stop is a no-op rather than a rewritten end time. */
|
|
327
|
+
readonly endImpersonationGrant: (sessionId: string, endedReason: string, now: Date) => Effect.Effect<boolean>;
|
|
218
328
|
/** Replace the user's recovery codes with a fresh (already hashed) set.
|
|
219
329
|
* Called at enrolment / regeneration — wipes any prior codes so the
|
|
220
330
|
* displayed set is the only valid one. */
|
package/dist/strategy.js
CHANGED
package/dist/tokens.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default email-verification lifetime — 24 hours.
|
|
3
|
+
*
|
|
4
|
+
* Longer than the other two on purpose, and the asymmetry is the point. A
|
|
5
|
+
* magic link and a reset link are CREDENTIALS: whoever holds one signs in, so
|
|
6
|
+
* a short fuse is worth the friction of asking for another. A verification
|
|
7
|
+
* link grants nothing — redeeming it only asserts that the address reaches its
|
|
8
|
+
* owner — so the cost of a short lifetime is all friction and no security, paid
|
|
9
|
+
* by exactly the users who check mail once a day.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EMAIL_VERIFY_TTL_S: number;
|
|
12
|
+
|
|
1
13
|
/** Hash a plaintext token for storage / lookup. SHA-256 is sufficient —
|
|
2
14
|
* the token is high-entropy (32 random bytes), so there's no
|
|
3
15
|
* brute-force surface that would need a slow KDF. */
|
|
@@ -6,6 +18,16 @@ export declare const hashToken: (plaintext: string) => string;
|
|
|
6
18
|
/** Default magic-link lifetime — short, since it's an inbox round-trip. */
|
|
7
19
|
export declare const MAGIC_LINK_TTL_S: number;
|
|
8
20
|
|
|
21
|
+
/** Lifetime of the short-lived MFA pending token (the second-factor
|
|
22
|
+
* challenge). Long enough for the user to fetch a TOTP code, short enough
|
|
23
|
+
* that a leaked challenge is near-useless.
|
|
24
|
+
*
|
|
25
|
+
* Lives HERE rather than beside the MFA handler that mints it, because the
|
|
26
|
+
* default-TTL table below has to be exhaustive over `TokenPurpose` and a
|
|
27
|
+
* constant imported from `handlers.ts` would be a cycle. `handlers.ts`
|
|
28
|
+
* re-exports it, so the public spelling is unchanged. */
|
|
29
|
+
export declare const MFA_PENDING_TTL_S: number;
|
|
30
|
+
|
|
9
31
|
export declare interface MintedToken {
|
|
10
32
|
/** The plaintext — put this in the emailed link. Never stored. */
|
|
11
33
|
readonly token: string;
|
|
@@ -16,6 +38,19 @@ export declare interface MintedToken {
|
|
|
16
38
|
readonly purpose: TokenPurpose;
|
|
17
39
|
}
|
|
18
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Mint a fresh single-use secret with NO purpose attached — 32 CSPRNG bytes,
|
|
43
|
+
* its SHA-256, and an expiry.
|
|
44
|
+
*
|
|
45
|
+
* Exists for the credentials that are single-use, hashed and expiring but do
|
|
46
|
+
* NOT live in `authTokens`: a tenant invitation carries its own row (with an
|
|
47
|
+
* address, a tenant and a role), so it has no `TokenPurpose` to name. Reaching
|
|
48
|
+
* for `mintToken('magic-link', …)` just to obtain the bytes would file an
|
|
49
|
+
* invitation under a purpose it does not have, in a type everything else reads
|
|
50
|
+
* as authoritative.
|
|
51
|
+
*/
|
|
52
|
+
export declare const mintSecret: (ttlSeconds: number) => Omit<MintedToken, "purpose">;
|
|
53
|
+
|
|
19
54
|
/**
|
|
20
55
|
* Mint a fresh single-use token. The caller persists `{ tokenHash,
|
|
21
56
|
* userId, purpose, expiresAt }` via `UserStore.insertToken` and emails
|
|
@@ -26,6 +61,6 @@ export declare const mintToken: (purpose: TokenPurpose, ttlSeconds?: number) =>
|
|
|
26
61
|
/** Default password-reset lifetime — a bit longer. */
|
|
27
62
|
export declare const PASSWORD_RESET_TTL_S: number;
|
|
28
63
|
|
|
29
|
-
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending';
|
|
64
|
+
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify';
|
|
30
65
|
|
|
31
66
|
export { }
|
package/dist/tokens.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { createHash as e, randomBytes as t } from "node:crypto";
|
|
2
2
|
//#region src/tokens.ts
|
|
3
|
-
var n = 900, r = 3600, i =
|
|
4
|
-
|
|
3
|
+
var n = 900, r = 3600, i = 1440 * 60, a = 300, o = {
|
|
4
|
+
"magic-link": 900,
|
|
5
|
+
"password-reset": r,
|
|
6
|
+
"mfa-pending": 300,
|
|
7
|
+
"email-verify": i
|
|
8
|
+
}, s = (e) => e.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), c = (t) => e("sha256").update(t).digest("hex"), l = (e) => {
|
|
9
|
+
let n = s(t(32));
|
|
5
10
|
return {
|
|
6
|
-
token:
|
|
7
|
-
tokenHash:
|
|
8
|
-
expiresAt: new Date(Date.now() +
|
|
9
|
-
purpose: e
|
|
11
|
+
token: n,
|
|
12
|
+
tokenHash: c(n),
|
|
13
|
+
expiresAt: new Date(Date.now() + e * 1e3)
|
|
10
14
|
};
|
|
11
|
-
}
|
|
15
|
+
}, u = (e, t) => ({
|
|
16
|
+
...l(t ?? o[e]),
|
|
17
|
+
purpose: e
|
|
18
|
+
});
|
|
12
19
|
//#endregion
|
|
13
|
-
export { n as MAGIC_LINK_TTL_S, r as PASSWORD_RESET_TTL_S,
|
|
20
|
+
export { i as EMAIL_VERIFY_TTL_S, n as MAGIC_LINK_TTL_S, a as MFA_PENDING_TTL_S, r as PASSWORD_RESET_TTL_S, c as hashToken, l as mintSecret, u as mintToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Authentication primitives: password hashing, session creation, schema mixin + tables. Pairs with the `auth` app template for the UI; both independently usable. Server-side only (uses node:crypto).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@effect/sql": "^0.52.0",
|
|
82
|
-
"@voltro/database": "0.
|
|
83
|
-
"@voltro/protocol": "0.
|
|
82
|
+
"@voltro/database": "0.34.0",
|
|
83
|
+
"@voltro/protocol": "0.34.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"effect": "^3.22.0",
|