@voltro/plugin-auth 0.85.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +5987 -0
  2. package/README.md +93 -0
  3. package/THIRD-PARTY-NOTICES.md +1122 -31
  4. package/dist/admission.js +9 -0
  5. package/dist/appUserStore.js +19 -0
  6. package/dist/authExtensions.js +9 -0
  7. package/dist/emailVerification.js +64 -0
  8. package/dist/guest.d.ts +99 -0
  9. package/dist/guest.js +3 -0
  10. package/dist/guestClient.js +237 -0
  11. package/dist/guestCredential.js +12 -0
  12. package/dist/guestPasswordUpgrade.js +85 -0
  13. package/dist/guestPlugin.js +166 -0
  14. package/dist/guestSchema.js +20 -0
  15. package/dist/guestSecrets.js +10 -0
  16. package/dist/guestService.js +224 -0
  17. package/dist/handlers.js +337 -0
  18. package/dist/impersonation.js +176 -0
  19. package/dist/index.d.ts +282 -20
  20. package/dist/index.js +30 -88
  21. package/dist/invitations.js +187 -0
  22. package/dist/mail.js +10 -0
  23. package/dist/memberships.js +8 -0
  24. package/dist/native/guestStorage.js +41 -0
  25. package/dist/native.d.ts +110 -0
  26. package/dist/native.js +45 -43
  27. package/dist/nativeRoutes.js +109 -0
  28. package/dist/overlayPlugin.js +167 -0
  29. package/dist/passkeyHandlers.js +162 -0
  30. package/dist/plugin.d.ts +123 -10
  31. package/dist/plugin.js +519 -5
  32. package/dist/postgresUserStore.js +475 -0
  33. package/dist/revocation.js +50 -0
  34. package/dist/schema.d.ts +45 -6
  35. package/dist/schema.js +99 -96
  36. package/dist/sessionFamily.js +15 -0
  37. package/dist/sessionRegistry.js +133 -0
  38. package/dist/signedClaims.js +24 -0
  39. package/dist/strategy.d.ts +17 -9
  40. package/dist/strategy.js +69 -3
  41. package/dist/subjectGuard.js +11 -0
  42. package/dist/totp.js +55 -0
  43. package/dist/userStore.js +292 -0
  44. package/dist/web/guestStorage.js +47 -0
  45. package/dist/web/passkeyClient.js +78 -0
  46. package/dist/web/requireAuth.js +9 -0
  47. package/dist/web/subjectContext.js +13 -0
  48. package/dist/web.d.ts +94 -0
  49. package/dist/web.js +6 -90
  50. package/package.json +8 -3
  51. package/dist/plugin-CbmcyEJs.js +0 -2018
  52. package/dist/strategy-XaGjSicX.js +0 -353
@@ -0,0 +1,187 @@
1
+ import { newAuthId as e } from "./userStore.js";
2
+ import { decideAdmission as t } from "./admission.js";
3
+ import { hashToken as n, mintSecret as r } from "./tokens.js";
4
+ import { Effect as i } from "effect";
5
+ //#region src/invitations.ts
6
+ var a = 604800, o = 500, s = [
7
+ "owner",
8
+ "admin",
9
+ "member",
10
+ "viewer"
11
+ ], c = ["owner", "admin"], l = (e, t) => t.indexOf(e), u = (e, t = s) => {
12
+ let n = l(e.inviterRole, t);
13
+ if (n < 0) return !1;
14
+ let r = l(e.requestedRole, t);
15
+ return r < 0 ? n === 0 : r >= n;
16
+ }, d = (e) => {
17
+ let t = e?.roleRank ?? s;
18
+ return e?.canGrantRole ?? ((e) => u(e, t));
19
+ }, f = (e, t, n = {}) => ({
20
+ status: e,
21
+ body: JSON.stringify(t),
22
+ contentType: "application/json; charset=utf-8",
23
+ ...n
24
+ }), p = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, m = (e) => ({
25
+ id: e.id,
26
+ email: e.email,
27
+ tenantId: e.tenantId,
28
+ role: e.role,
29
+ invitedByUserId: e.invitedByUserId,
30
+ expiresAt: e.expiresAt.toISOString(),
31
+ acceptedAt: e.acceptedAt?.toISOString() ?? null,
32
+ revokedAt: e.revokedAt?.toISOString() ?? null,
33
+ createdAt: e.createdAt.toISOString()
34
+ }), h = (e, t, n, r) => i.gen(function* () {
35
+ let i = yield* n.membershipRole(e, t);
36
+ return i === null || !(r?.inviterRoles ?? c).includes(i) ? f(403, { error: "not_permitted" }) : { role: i };
37
+ }), g = (e) => "status" in e, _ = (t, n, a) => i.gen(function* () {
38
+ let o = t.email.trim().toLowerCase();
39
+ if (!o || !o.includes("@")) return f(400, { error: "a valid email is required" });
40
+ if (!t.tenantId) return f(400, { error: "tenantId required" });
41
+ if (!t.role) return f(400, { error: "role required" });
42
+ let s = a.invitations, c = yield* h(t.inviterUserId, t.tenantId, n, s);
43
+ if (g(c)) return c;
44
+ if (!d(s)({
45
+ inviterRole: c.role,
46
+ requestedRole: t.role,
47
+ tenantId: t.tenantId
48
+ })) return f(403, {
49
+ error: "role_not_grantable",
50
+ message: `A '${c.role}' may not invite a '${t.role}'.`
51
+ });
52
+ let l = new Date((a.now ?? Date.now)());
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
+ id: e("inv"),
57
+ tokenHash: _.tokenHash,
58
+ email: o,
59
+ tenantId: t.tenantId,
60
+ role: t.role,
61
+ invitedByUserId: t.inviterUserId,
62
+ expiresAt: new Date(l.getTime() + u * 1e3)
63
+ });
64
+ if (a.sendEmail) {
65
+ let e = p(a.appBaseUrl ?? "", s?.callbackPath ?? "/auth/invitations/accept", _.token);
66
+ yield* i.tryPromise(() => a.sendEmail({
67
+ to: o,
68
+ kind: "invitation",
69
+ actionUrl: e,
70
+ subject: "You have been invited",
71
+ text: `You have been invited to join as ${t.role}: ${e}`,
72
+ html: `<p>You have been invited to join as <strong>${t.role}</strong>. <a href="${e}">Accept the invitation</a> — this link expires.</p>`
73
+ })).pipe(i.ignore);
74
+ }
75
+ return f(201, {
76
+ ok: !0,
77
+ invitation: m(v)
78
+ });
79
+ }), v = (e, t, n) => i.gen(function* () {
80
+ let r = yield* h(e.callerUserId, e.tenantId, t, n.invitations);
81
+ return g(r) ? r : f(200, { invitations: (yield* t.listInvitations(e.tenantId)).map(m) });
82
+ }), y = (e, t, n) => i.gen(function* () {
83
+ if (!e.invitationId) return f(400, { error: "invitationId required" });
84
+ let r = yield* h(e.callerUserId, e.tenantId, t, n.invitations);
85
+ if (g(r)) return r;
86
+ let i = new Date((n.now ?? Date.now)());
87
+ return (yield* t.revokeInvitation(e.invitationId, e.tenantId, e.callerUserId, i)) ? f(200, {
88
+ ok: !0,
89
+ revoked: 1
90
+ }) : f(404, { error: "invitation_not_found" });
91
+ }), b = (e, t, r = {}) => i.gen(function* () {
92
+ if (!e.token) return f(400, { error: "token required" });
93
+ let i = yield* t.findInvitationByHash(n(e.token));
94
+ if (!i) return f(404, { error: "invalid_invitation" });
95
+ let a = new Date((r.now ?? Date.now)());
96
+ return f(200, {
97
+ ok: !0,
98
+ state: i.acceptedAt === null ? i.revokedAt === null ? i.expiresAt.getTime() <= a.getTime() ? "expired" : "pending" : "revoked" : "accepted",
99
+ email: i.email,
100
+ tenantId: i.tenantId,
101
+ role: i.role,
102
+ expiresAt: i.expiresAt.toISOString()
103
+ });
104
+ }), x = (e, r, a = {}) => i.gen(function* () {
105
+ if (!e.token) return f(400, { error: "token required" });
106
+ let o = n(e.token), s = new Date((a.now ?? Date.now)()), c = yield* r.findInvitationByHash(o);
107
+ if (!c) return f(401, { error: "invalid_or_expired_invitation" });
108
+ if (!e.userId) return f(401, {
109
+ error: "authentication_required",
110
+ email: c.email
111
+ });
112
+ let l = yield* r.findById(e.userId);
113
+ if (!l) return f(401, {
114
+ error: "authentication_required",
115
+ email: c.email
116
+ });
117
+ if (l.email === null || l.email.toLowerCase() !== c.email.toLowerCase()) return f(403, {
118
+ error: "invitation_email_mismatch",
119
+ message: "This invitation was issued to a different email address. Sign in as that address to accept it."
120
+ });
121
+ let u = yield* t(a.admit, {
122
+ email: c.email,
123
+ tenantId: c.tenantId,
124
+ via: "invitation"
125
+ });
126
+ if (u) return f(403, {
127
+ error: "not_admitted",
128
+ reason: u.reason,
129
+ message: u.message
130
+ });
131
+ let d = yield* r.acceptInvitation(o, l.id, s);
132
+ if (!d) return f(401, { error: "invalid_or_expired_invitation" });
133
+ let p = yield* r.addMembership({
134
+ userId: l.id,
135
+ tenantId: d.tenantId,
136
+ role: d.role
137
+ });
138
+ return yield* r.markEmailVerified(l.id).pipe(i.ignore), f(200, {
139
+ ok: !0,
140
+ membership: {
141
+ tenantId: p.tenantId,
142
+ role: p.role,
143
+ joinedAt: p.joinedAt.toISOString()
144
+ }
145
+ });
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
+ if (!l) return f(401, { error: "invalid_or_expired_invitation" });
151
+ if (yield* a.findByEmail(l.email)) return f(409, {
152
+ error: "account_exists",
153
+ message: "An account already exists for this address. Sign in, then accept the invitation."
154
+ });
155
+ let u = yield* o.hashPassword(r.password).pipe(i.orElseSucceed(() => ""));
156
+ if (!u) return f(500, { error: "hash_failed" });
157
+ let d = yield* t(o.admit, {
158
+ email: l.email,
159
+ tenantId: l.tenantId,
160
+ via: "invitation"
161
+ });
162
+ if (d) return f(403, {
163
+ error: "not_admitted",
164
+ reason: d.reason,
165
+ message: d.message
166
+ });
167
+ let p = e("user"), m = yield* a.acceptInvitation(s, p, c);
168
+ if (!m) return f(401, { error: "invalid_or_expired_invitation" });
169
+ let h = yield* a.insert({
170
+ id: p,
171
+ email: m.email,
172
+ passwordHash: u,
173
+ tenantId: m.tenantId
174
+ }).pipe(i.orElseSucceed(() => null));
175
+ return h ? (yield* a.addMembership({
176
+ userId: h.id,
177
+ tenantId: m.tenantId,
178
+ role: m.role
179
+ }), yield* a.markEmailVerified(h.id).pipe(i.ignore), f(201, {
180
+ ok: !0,
181
+ userId: h.id,
182
+ tenantId: m.tenantId,
183
+ role: m.role
184
+ })) : f(409, { error: "account_exists" });
185
+ });
186
+ //#endregion
187
+ export { c as DEFAULT_INVITER_ROLES, s as DEFAULT_ROLE_RANK, o as INVITATION_MAX_PENDING, a as INVITATION_TTL_S, u as defaultCanGrantRole, x as handleAcceptInvitation, S as handleAcceptInvitationWithSignUp, _ as handleCreateInvitation, v as handleListInvitations, b as handlePreviewInvitation, y as handleRevokeInvitation };
package/dist/mail.js ADDED
@@ -0,0 +1,10 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/mail.ts
3
+ var t = (t) => (n) => e.runPromise(t.send({
4
+ to: n.to,
5
+ subject: n.subject,
6
+ html: n.html,
7
+ text: n.text
8
+ }).pipe(e.asVoid, e.orElseSucceed(() => void 0)));
9
+ //#endregion
10
+ export { t as mailSender };
@@ -0,0 +1,8 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/memberships.ts
3
+ var t = (t) => async (n) => n.type === "user" ? (await e.runPromise(t.listMemberships(n.id))).map((e) => ({
4
+ tenantId: e.tenantId,
5
+ role: e.role
6
+ })) : [];
7
+ //#endregion
8
+ export { t as authMemberships };
@@ -0,0 +1,41 @@
1
+ import { GuestClientError as e } from "../guestClient.js";
2
+ //#region src/native/guestStorage.ts
3
+ var t = /* @__PURE__ */ new Set([
4
+ "keychain",
5
+ "keystore",
6
+ "credential-manager",
7
+ "stronghold"
8
+ ]), n = () => new e("storage_unavailable"), r = (r) => {
9
+ let i;
10
+ try {
11
+ if (!r.namespace.trim() || !t.has(r.store.kind) || typeof r.exclusive != "function" || typeof r.store.get != "function" || typeof r.store.set != "function" || typeof r.store.delete != "function") throw n();
12
+ i = "voltro:guest:" + encodeURIComponent(r.namespace);
13
+ } catch {
14
+ throw n();
15
+ }
16
+ let a = async (e) => {
17
+ try {
18
+ return await e();
19
+ } catch {
20
+ throw n();
21
+ }
22
+ };
23
+ return {
24
+ exclusive: async (t) => {
25
+ try {
26
+ return await r.exclusive(i, t);
27
+ } catch (t) {
28
+ throw t instanceof e ? t : n();
29
+ }
30
+ },
31
+ read: () => a(async () => {
32
+ let e = await r.store.get(i);
33
+ if (e !== null && typeof e != "string") throw n();
34
+ return e;
35
+ }),
36
+ write: (e) => a(() => r.store.set(i, e)),
37
+ remove: () => a(() => r.store.delete(i))
38
+ };
39
+ };
40
+ //#endregion
41
+ export { r as nativeGuestStore };
package/dist/native.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Subject } from '@voltro/protocol';
2
+ import { SubjectIdentity } from '@voltro/protocol';
2
3
 
3
4
  /**
4
5
  * Mint the verifier + state and build the authorize URL. Nothing is sent yet;
@@ -6,6 +7,40 @@ import { Subject } from '@voltro/protocol';
6
7
  */
7
8
  export declare const beginNativeLogin: (options: NativeLoginOptions) => Promise<NativeLoginHandoff>;
8
9
 
10
+ /** Serializes operations in one client. A rejected existing credential never
11
+ * creates a replacement identity. Storage failures never enable memory fallback. */
12
+ export declare const createGuestClient: (options: GuestClientOptions) => {
13
+ connect: () => Promise<GuestClientSession>;
14
+ /** Prepare without replacing the active proof, persist both proofs, then
15
+ * commit. A later connect recovers the same identity after a lost response. */
16
+ rotate: () => Promise<GuestClientSession>;
17
+ /** Revoke on the server first. Keep the durable proof on transport failure
18
+ * so the caller can retry; erase locally only after confirmed revocation. */
19
+ revoke: () => Promise<void>;
20
+ /** Turn this guest into an email/password account under the same subject ID
21
+ * (`authRoutesPlugin({ guestUpgrade })`). The device proof is erased only after the
22
+ * server confirmed the account; on any refusal or transport failure it stays. */
23
+ upgrade: (input: {
24
+ readonly email: string;
25
+ readonly password: string;
26
+ }) => Promise<{
27
+ readonly subject: unknown;
28
+ }>;
29
+ /** The registered account signed in on this device (cookie clients), or null. */
30
+ account: () => Promise<GuestClientAccount | null>;
31
+ /** The signed-in account if there is one, else this device's guest (resumed or created once).
32
+ * The usual entry for a game that offers accounts: a reload after `upgrade` or `signIn` stays the account. */
33
+ start: () => Promise<GuestClientPlayer>;
34
+ /** Sign in to an existing account with email and password (`authRoutesPlugin`), e.g. on a second
35
+ * device. This device's guest proof stays: after `signOut`, `start` resumes that guest. */
36
+ signIn: (input: {
37
+ readonly email: string;
38
+ readonly password: string;
39
+ }) => Promise<GuestClientAccount>;
40
+ /** End the account session on the server and clear its cookie. */
41
+ signOut: () => Promise<void>;
42
+ };
43
+
9
44
  /** Trade the code + verifier for the session value. */
10
45
  export declare const exchangeNativeCode: (input: {
11
46
  readonly code: string;
@@ -14,6 +49,61 @@ export declare const exchangeNativeCode: (input: {
14
49
  readonly fetch?: typeof fetch;
15
50
  }) => Promise<NativeSession>;
16
51
 
52
+ /** A registered account signed in on this device (by upgrade or sign-in), through the session cookie. */
53
+ declare interface GuestClientAccount {
54
+ readonly subject: Extract<SubjectIdentity, {
55
+ readonly type: 'user';
56
+ }>;
57
+ }
58
+
59
+ export declare class GuestClientError extends Error {
60
+ readonly code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response';
61
+ readonly status?: number | undefined;
62
+ constructor(code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response', status?: number | undefined);
63
+ }
64
+
65
+ export declare interface GuestClientOptions {
66
+ readonly apiBaseUrl: string;
67
+ readonly storage: GuestCredentialStore;
68
+ readonly prefix?: string;
69
+ readonly fetch?: typeof globalThis.fetch;
70
+ /** Web clients include cookies and obtain a signed CSRF token for each post. */
71
+ readonly cookies?: boolean;
72
+ /** Prefix of the app's auth routes, for `upgrade`, `account`, `signIn` and `signOut` (default `/auth`). */
73
+ readonly accountPrefix?: string;
74
+ }
75
+
76
+ /** What `start` found: the signed-in account, else this device's guest. */
77
+ declare type GuestClientPlayer = {
78
+ readonly kind: 'account';
79
+ readonly subject: GuestClientAccount['subject'];
80
+ } | {
81
+ readonly kind: 'guest';
82
+ readonly subject: GuestClientSession['subject'];
83
+ readonly session: string;
84
+ };
85
+
86
+ export declare interface GuestClientSession {
87
+ readonly subject: Extract<SubjectIdentity, {
88
+ readonly type: 'user';
89
+ }> & {
90
+ readonly kind: 'guest';
91
+ };
92
+ readonly session: string;
93
+ }
94
+
95
+ /** Platform-backed durable secret storage. Native implementations use the
96
+ * operating system's secure store; writes resolve only after persistence. */
97
+ export declare interface GuestCredentialStore {
98
+ /** Serialize the whole read/create/write operation across users of this
99
+ * namespace, including other browser tabs or native client instances. */
100
+ readonly exclusive: <A>(work: () => Promise<A>) => Promise<A>;
101
+ readonly read: () => Promise<string | null>;
102
+ /** Persist the opaque value, including an in-progress rotation journal. */
103
+ readonly write: (value: string) => Promise<void>;
104
+ readonly remove: () => Promise<void>;
105
+ }
106
+
17
107
  export declare type NativeCallback = {
18
108
  readonly kind: 'code';
19
109
  readonly code: string;
@@ -40,6 +130,26 @@ export declare class NativeExchangeError extends Error {
40
130
  constructor(status: number, code: string);
41
131
  }
42
132
 
133
+ /** Adapt an explicitly supplied durable native store and host-wide lock.
134
+ * Refuse memory/unwired stores; never select a fallback or initialize a store. */
135
+ export declare const nativeGuestStore: (options: NativeGuestStoreOptions) => GuestCredentialStore;
136
+
137
+ /** Structurally accepts a platform TokenStore without importing a UI runtime. */
138
+ export declare interface NativeGuestStoreOptions {
139
+ /** App/tenant namespace; storage and the host lock use the same exact key. */
140
+ readonly namespace: string;
141
+ readonly store: {
142
+ readonly kind: string;
143
+ readonly get: (key: string) => Promise<string | null>;
144
+ readonly set: (key: string, value: string) => Promise<void>;
145
+ readonly delete: (key: string) => Promise<void>;
146
+ };
147
+ /** The host must serialize all client instances, including other processes.
148
+ * Release the lock after both success and failure; writes must be durable
149
+ * before resolving. A process-local Promise queue does not meet this contract. */
150
+ readonly exclusive: <A>(key: string, work: () => Promise<A>) => Promise<A>;
151
+ }
152
+
43
153
  export declare interface NativeLoginHandoff {
44
154
  /** Open this in the SYSTEM browser (not an in-app webview): the user's
45
155
  * existing browser session and password manager are the point. */
package/dist/native.js CHANGED
@@ -1,42 +1,44 @@
1
- import { SESSION_HEADER_NAME as e } from "@voltro/protocol";
1
+ import { GuestClientError as e, createGuestClient as t } from "./guestClient.js";
2
+ import { nativeGuestStore as n } from "./native/guestStorage.js";
3
+ import { SESSION_HEADER_NAME as r } from "@voltro/protocol";
2
4
  //#region src/native/index.ts
3
- var t = (e) => {
5
+ var i = (e) => {
4
6
  let t = "";
5
7
  for (let n of e) t += String.fromCharCode(n);
6
8
  return (typeof btoa == "function" ? btoa(t) : globalThis.Buffer.from(t, "binary").toString("base64")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7
- }, n = (e) => {
9
+ }, a = (e) => {
8
10
  let t = new Uint8Array(e.length);
9
11
  for (let n = 0; n < e.length; n++) t[n] = e.charCodeAt(n) & 127;
10
12
  return t;
11
- }, r = (e) => e.replace(/\/+$/, ""), i = async (e, r) => t(await r.sha256(n(e))), a = async (e) => {
12
- let n = t(e.crypto.randomBytes(32)), a = t(e.crypto.randomBytes(16)), o = await i(n, e.crypto), s = new URLSearchParams({
13
+ }, o = (e) => e.replace(/\/+$/, ""), s = async (e, t) => i(await t.sha256(a(e))), c = async (e) => {
14
+ let t = i(e.crypto.randomBytes(32)), n = i(e.crypto.randomBytes(16)), r = await s(t, e.crypto), a = new URLSearchParams({
13
15
  redirect_uri: e.redirectUri,
14
- code_challenge: o,
16
+ code_challenge: r,
15
17
  code_challenge_method: "S256",
16
- state: a
18
+ state: n
17
19
  }), c = e.prefix ?? "/auth";
18
20
  return {
19
- url: `${r(e.apiBaseUrl)}${c}/native/authorize?${s.toString()}`,
20
- codeVerifier: n,
21
- state: a
21
+ url: `${o(e.apiBaseUrl)}${c}/native/authorize?${a.toString()}`,
22
+ codeVerifier: t,
23
+ state: n
22
24
  };
23
- }, o = (e, t, n) => {
24
- let i = e.indexOf("?");
25
- if (r(i === -1 ? e : e.slice(0, i)) !== r(n)) return {
25
+ }, l = (e, t, n) => {
26
+ let r = e.indexOf("?");
27
+ if (o(r === -1 ? e : e.slice(0, r)) !== o(n)) return {
26
28
  kind: "ignored",
27
29
  reason: "not-the-callback"
28
30
  };
29
- let a = new URLSearchParams(i === -1 ? "" : e.slice(i + 1));
30
- if (a.get("state") !== t.state) return {
31
+ let i = new URLSearchParams(r === -1 ? "" : e.slice(r + 1));
32
+ if (i.get("state") !== t.state) return {
31
33
  kind: "ignored",
32
34
  reason: "state-mismatch"
33
35
  };
34
- let o = a.get("error");
35
- if (o) return {
36
+ let a = i.get("error");
37
+ if (a) return {
36
38
  kind: "error",
37
- error: o
39
+ error: a
38
40
  };
39
- let s = a.get("code");
41
+ let s = i.get("code");
40
42
  return s ? {
41
43
  kind: "code",
42
44
  code: s
@@ -44,43 +46,43 @@ var t = (e) => {
44
46
  kind: "ignored",
45
47
  reason: "no-code"
46
48
  };
47
- }, s = class extends Error {
49
+ }, u = class extends Error {
48
50
  status;
49
51
  code;
50
52
  constructor(e, t) {
51
53
  super(`native login exchange failed: ${t} (${e})`), this.name = "NativeExchangeError", this.status = e, this.code = t;
52
54
  }
53
- }, c = async (e, t) => {
54
- let n = t.fetch ?? fetch, i = t.prefix ?? "/auth", a = await n(`${r(t.apiBaseUrl)}${i}/native/exchange`, {
55
+ }, d = async (e, t) => {
56
+ let n = t.fetch ?? fetch, r = t.prefix ?? "/auth", i = await n(`${o(t.apiBaseUrl)}${r}/native/exchange`, {
55
57
  method: "POST",
56
58
  headers: { "content-type": "application/json" },
57
59
  body: JSON.stringify(e)
58
- }), o = await a.json().catch(() => ({}));
59
- if (!a.ok || typeof o.session != "string" || typeof o.expiresAt != "number" || !o.subject) throw new s(a.status, o.error ?? "invalid_response");
60
+ }), a = await i.json().catch(() => ({}));
61
+ if (!i.ok || typeof a.session != "string" || typeof a.expiresAt != "number" || !a.subject) throw new u(i.status, a.error ?? "invalid_response");
60
62
  return {
61
- session: o.session,
62
- expiresAt: o.expiresAt,
63
- subject: o.subject
63
+ session: a.session,
64
+ expiresAt: a.expiresAt,
65
+ subject: a.subject
64
66
  };
65
- }, l = (t) => ({ [e]: t }), u = async (e, t) => {
66
- let n = t.fetch ?? fetch, i = t.prefix ?? "/auth", a = await n(`${r(t.apiBaseUrl)}${i}/session`, {
67
+ }, f = (e) => ({ [r]: e }), p = async (e, t) => {
68
+ let n = t.fetch ?? fetch, r = t.prefix ?? "/auth", i = await n(`${o(t.apiBaseUrl)}${r}/session`, {
67
69
  method: "GET",
68
- headers: l(e)
70
+ headers: f(e)
69
71
  });
70
- if (a.status === 401) return null;
71
- let o = await a.json().catch(() => ({}));
72
- return !a.ok || o.ok !== !0 || !o.subject || typeof o.expiresAt != "number" ? null : {
73
- session: o.session ?? e,
74
- expiresAt: o.expiresAt,
75
- subject: o.subject
72
+ if (i.status === 401) return null;
73
+ let a = await i.json().catch(() => ({}));
74
+ return !i.ok || a.ok !== !0 || !a.subject || typeof a.expiresAt != "number" ? null : {
75
+ session: a.session ?? e,
76
+ expiresAt: a.expiresAt,
77
+ subject: a.subject
76
78
  };
77
- }, d = async (e) => {
78
- let t = await a(e);
79
+ }, m = async (e) => {
80
+ let t = await c(e);
79
81
  await e.open(t.url);
80
- let n = await e.awaitCallback((n) => o(n, t, e.redirectUri));
81
- if (n.kind === "error") throw new s(0, n.error);
82
- if (n.kind !== "code") throw new s(0, n.reason);
83
- return c({
82
+ let n = await e.awaitCallback((n) => l(n, t, e.redirectUri));
83
+ if (n.kind === "error") throw new u(0, n.error);
84
+ if (n.kind !== "code") throw new u(0, n.reason);
85
+ return d({
84
86
  code: n.code,
85
87
  codeVerifier: t.codeVerifier
86
88
  }, {
@@ -90,4 +92,4 @@ var t = (e) => {
90
92
  });
91
93
  };
92
94
  //#endregion
93
- export { s as NativeExchangeError, a as beginNativeLogin, c as exchangeNativeCode, i as nativeCodeChallenge, o as parseNativeCallback, u as probeNativeSession, d as runNativeLogin, l as sessionHeaders };
95
+ export { e as GuestClientError, u as NativeExchangeError, c as beginNativeLogin, t as createGuestClient, d as exchangeNativeCode, s as nativeCodeChallenge, n as nativeGuestStore, l as parseNativeCallback, p as probeNativeSession, m as runNativeLogin, f as sessionHeaders };
@@ -0,0 +1,109 @@
1
+ import { hashToken as e, mintToken as t } from "./tokens.js";
2
+ import { configSessionSecrets as n, issueUserSession as r } from "./handlers.js";
3
+ import { signAuthClaims as i, verifyAuthClaims as a } from "./signedClaims.js";
4
+ import { createHash as o, randomBytes as s } from "node:crypto";
5
+ import { Effect as c } from "effect";
6
+ import { buildSetCookie as l, readCookie as u } from "@voltro/protocol/session";
7
+ //#region src/nativeRoutes.ts
8
+ var d = "voltro:native", f = 600, p = "/login", m = (e) => e.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), h = (e) => m(o("sha256").update(e, "ascii").digest()), g = (e, t) => t.includes(e), _ = (e, t) => {
9
+ let n = new URLSearchParams(t).toString();
10
+ return `${e}${e.includes("?") ? "&" : "?"}${n}`;
11
+ }, v = (e, t) => ({
12
+ status: e,
13
+ body: JSON.stringify(t),
14
+ contentType: "application/json; charset=utf-8"
15
+ }), y = (e, t) => ({
16
+ status: 302,
17
+ body: "",
18
+ location: e,
19
+ ...t === void 0 ? {} : { setCookie: t }
20
+ }), b = (e) => {
21
+ if (!e) return null;
22
+ let { ch: t, r: n, s: r, exp: i } = e;
23
+ return typeof t != "string" || typeof n != "string" || typeof r != "string" || typeof i != "number" ? null : {
24
+ ch: t,
25
+ r: n,
26
+ s: r,
27
+ exp: i
28
+ };
29
+ }, x = (e, t, r) => {
30
+ let i = n(r.config);
31
+ return [i.current, i.previous].filter((e) => e !== void 0).map((n) => a(e, n.secret, t, r.now)).find((e) => e !== null) ?? null;
32
+ }, S = (e, t, n) => l(e.native.pendingCookieName ?? "voltro:native", t, {
33
+ maxAgeSeconds: n,
34
+ httpOnly: !0,
35
+ sameSite: "lax",
36
+ secure: e.config.cookieSecure ?? !0,
37
+ path: `${e.prefix}/native`
38
+ }), C = (e) => `${e.native.loginPath ?? p}?next=${encodeURIComponent(`${e.prefix}/native/complete`)}`, w = (e, t, r, a) => c.gen(function* () {
39
+ let o = e.get("redirect_uri") ?? "", s = e.get("code_challenge") ?? "", c = e.get("code_challenge_method") ?? "S256", l = e.get("state") ?? "";
40
+ if (!g(o, a.native.redirectUris)) return v(400, { error: "redirect_uri_not_allowed" });
41
+ if (c !== "S256" || s.length < 43 || s.length > 128) return y(_(o, {
42
+ error: "invalid_request",
43
+ state: l
44
+ }));
45
+ if (l.length === 0) return y(_(o, { error: "invalid_request" }));
46
+ let u = a.now ?? Date.now, d = a.native.pendingTtlSeconds ?? f, p = {
47
+ ch: s,
48
+ r: o,
49
+ s: l,
50
+ exp: Math.floor(u() / 1e3) + d
51
+ }, m = S(a, i({ ...p }, n(a.config).current.secret, "native-pending"), d);
52
+ return t && t.subject.type === "user" && t.sessionId ? {
53
+ ...yield* T(p, {
54
+ userId: t.subject.id,
55
+ sessionId: t.sessionId
56
+ }, r, a),
57
+ setCookie: S(a, "", 0)
58
+ } : y(C(a), m);
59
+ }), T = (e, r, a, o) => c.gen(function* () {
60
+ let c = t("native-code", 300);
61
+ yield* a.insertToken({
62
+ id: `tok_${m(s(12))}`,
63
+ tokenHash: c.tokenHash,
64
+ userId: r.userId,
65
+ purpose: "native-code",
66
+ expiresAt: c.expiresAt
67
+ });
68
+ let l = i({
69
+ sid: r.sessionId,
70
+ uid: r.userId,
71
+ ch: e.ch,
72
+ exp: Math.floor(c.expiresAt.getTime() / 1e3)
73
+ }, n(o.config).current.secret, "native-code");
74
+ return y(_(e.r, {
75
+ code: `${c.token}.${l}`,
76
+ state: e.s
77
+ }));
78
+ }), E = (e, t, n, r) => c.gen(function* () {
79
+ let i = b(x(u(e, r.native.pendingCookieName ?? "voltro:native"), "native-pending", r));
80
+ return i ? !t || t.subject.type !== "user" || !t.sessionId ? y(C(r)) : {
81
+ ...yield* T(i, {
82
+ userId: t.subject.id,
83
+ sessionId: t.sessionId
84
+ }, n, r),
85
+ setCookie: S(r, "", 0)
86
+ } : v(400, { error: "no_pending_native_login" });
87
+ }), D = (t, n, i) => c.gen(function* () {
88
+ let a = t.code.indexOf(".");
89
+ if (a <= 0) return v(400, { error: "invalid_code" });
90
+ let o = t.code.slice(0, a), s = x(t.code.slice(a + 1), "native-code", i), c = s?.sid, l = s?.uid, u = s?.ch;
91
+ if (typeof c != "string" || typeof l != "string" || typeof u != "string") return v(400, { error: "invalid_code" });
92
+ let d = yield* n.consumeToken(e(o), "native-code");
93
+ if (!d || d.userId !== l || t.codeVerifier.length < 43 || h(t.codeVerifier) !== u) return v(400, { error: "invalid_code" });
94
+ let f = yield* n.findById(l);
95
+ if (!f) return v(400, { error: "invalid_code" });
96
+ let p = yield* r(f, n, i.config, {
97
+ sessionId: c,
98
+ sessionRow: "existing",
99
+ clearLoginFailures: !1
100
+ });
101
+ return v(200, {
102
+ ok: !0,
103
+ session: p.value,
104
+ expiresAt: p.expiresAt,
105
+ subject: p.subject
106
+ });
107
+ });
108
+ //#endregion
109
+ export { d as NATIVE_PENDING_COOKIE, w as handleNativeAuthorize, E as handleNativeComplete, D as handleNativeExchange, g as isAllowedRedirectUri, C as nativeLoginRedirect, h as s256 };