@sovereignfs/sdk 1.25.0 → 2.0.1

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 (55) hide show
  1. package/LICENSE +373 -661
  2. package/README.md +103 -0
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +4 -0
  5. package/dist/chunk-NYL4LLXF.js +65 -0
  6. package/dist/index.d.ts +57 -529
  7. package/dist/index.js +1283 -572
  8. package/dist/next.d.ts +37 -0
  9. package/dist/next.js +68 -0
  10. package/dist/session-DJl2XYMc.d.ts +44 -0
  11. package/dist/sovereign-CazGKEVc.d.ts +517 -0
  12. package/dist/testing.d.ts +181 -0
  13. package/dist/testing.js +560 -0
  14. package/package.json +53 -27
  15. package/dist/chunk-MLKGABMK.js +0 -9
  16. package/dist/chunk-NQLGPAL6.js +0 -62
  17. package/dist/e2ee-crypto.d.ts +0 -84
  18. package/dist/e2ee-crypto.js +0 -162
  19. package/dist/e2ee-device.d.ts +0 -21
  20. package/dist/e2ee-device.js +0 -13
  21. package/dist/e2ee-object.d.ts +0 -50
  22. package/dist/e2ee-object.js +0 -75
  23. package/dist/e2ee-state-U-GEJ7oL.d.ts +0 -411
  24. package/dist/e2ee-state.d.ts +0 -1
  25. package/dist/e2ee-state.js +0 -36
  26. package/src/__tests__/e2ee-crypto.test.ts +0 -156
  27. package/src/__tests__/e2ee-object.test.ts +0 -90
  28. package/src/__tests__/e2ee-state.test.ts +0 -83
  29. package/src/__tests__/sdk.test.ts +0 -427
  30. package/src/__tests__/sessions.test.ts +0 -63
  31. package/src/activity.ts +0 -19
  32. package/src/auth.ts +0 -134
  33. package/src/connections.ts +0 -106
  34. package/src/data.ts +0 -88
  35. package/src/db.ts +0 -34
  36. package/src/directory.ts +0 -31
  37. package/src/e2ee-crypto.ts +0 -260
  38. package/src/e2ee-device.ts +0 -75
  39. package/src/e2ee-object.ts +0 -118
  40. package/src/e2ee-state.ts +0 -72
  41. package/src/e2ee.ts +0 -65
  42. package/src/env.ts +0 -37
  43. package/src/errors.ts +0 -46
  44. package/src/host.ts +0 -251
  45. package/src/index.ts +0 -160
  46. package/src/mailer.ts +0 -7
  47. package/src/notifications.ts +0 -36
  48. package/src/platform.ts +0 -10
  49. package/src/plugins.ts +0 -146
  50. package/src/portability.ts +0 -176
  51. package/src/secrets.ts +0 -68
  52. package/src/sessions.ts +0 -36
  53. package/src/storage.ts +0 -49
  54. package/src/types.ts +0 -418
  55. package/src/unimplemented.ts +0 -48
package/dist/index.js CHANGED
@@ -1,652 +1,1363 @@
1
1
  import {
2
- __export
3
- } from "./chunk-MLKGABMK.js";
2
+ ConfigurationError,
3
+ CoreApiError,
4
+ CoreUnavailableError,
5
+ NotAuthenticatedError,
6
+ NotSupportedError,
7
+ SessionEndedError,
8
+ SignInError,
9
+ SovereignError
10
+ } from "./chunk-NYL4LLXF.js";
4
11
 
5
- // src/auth.ts
6
- var auth_exports = {};
7
- __export(auth_exports, {
8
- changePassword: () => changePassword,
9
- getSession: () => getSession,
10
- hasCapability: () => hasCapability,
11
- listSessions: () => listSessions,
12
- requireSession: () => requireSession,
13
- revokeSession: () => revokeSession,
14
- signOut: () => signOut
15
- });
16
- import { headers } from "next/headers";
12
+ // ../api-client/src/generated/operations.ts
13
+ var operations = {
14
+ dismissAllNotifications: {
15
+ method: "POST",
16
+ path: "/api/v1/notifications/dismiss-all",
17
+ scopes: ["notifications"],
18
+ safeToRepeat: false
19
+ },
20
+ dismissNotification: {
21
+ method: "POST",
22
+ path: "/api/v1/notifications/{id}/dismiss",
23
+ scopes: ["notifications"],
24
+ safeToRepeat: false
25
+ },
26
+ findDirectoryUserByEmail: {
27
+ method: "GET",
28
+ path: "/api/v1/directory/users/by-email",
29
+ scopes: ["directory"],
30
+ safeToRepeat: true
31
+ },
32
+ getInstanceBranding: {
33
+ method: "GET",
34
+ path: "/.well-known/sovereign-instance",
35
+ scopes: void 0,
36
+ safeToRepeat: true
37
+ },
38
+ listNotifications: {
39
+ method: "GET",
40
+ path: "/api/v1/notifications",
41
+ scopes: ["notifications"],
42
+ safeToRepeat: true
43
+ },
44
+ logActivity: {
45
+ method: "POST",
46
+ path: "/api/v1/activity",
47
+ scopes: ["activity:write"],
48
+ safeToRepeat: false
49
+ },
50
+ markAllNotificationsRead: {
51
+ method: "POST",
52
+ path: "/api/v1/notifications/read-all",
53
+ scopes: ["notifications"],
54
+ safeToRepeat: false
55
+ },
56
+ markNotificationRead: {
57
+ method: "POST",
58
+ path: "/api/v1/notifications/{id}/read",
59
+ scopes: ["notifications"],
60
+ safeToRepeat: false
61
+ },
62
+ resolveDirectoryUsers: {
63
+ method: "POST",
64
+ path: "/api/v1/directory/users/resolve",
65
+ scopes: ["directory"],
66
+ safeToRepeat: true
67
+ },
68
+ searchDirectoryUsers: {
69
+ method: "GET",
70
+ path: "/api/v1/directory/users",
71
+ scopes: ["directory"],
72
+ safeToRepeat: true
73
+ },
74
+ sendEmail: {
75
+ method: "POST",
76
+ path: "/api/v1/email",
77
+ scopes: ["email:send"],
78
+ safeToRepeat: false
79
+ },
80
+ sendNotification: {
81
+ method: "POST",
82
+ path: "/api/v1/notifications",
83
+ scopes: ["notifications:send"],
84
+ safeToRepeat: false
85
+ }
86
+ };
17
87
 
18
- // src/errors.ts
19
- var NotImplementedError = class extends Error {
20
- constructor(message) {
88
+ // ../api-client/src/client.ts
89
+ var CoreApiError2 = class extends Error {
90
+ constructor(status, code, message, retryAfter) {
21
91
  super(message);
22
- this.name = "NotImplementedError";
92
+ this.status = status;
93
+ this.code = code;
94
+ this.retryAfter = retryAfter;
23
95
  }
96
+ status;
97
+ code;
98
+ retryAfter;
99
+ name = "CoreApiError";
24
100
  };
25
- var NotAuthenticatedError = class extends Error {
26
- constructor() {
27
- super("No authenticated session. The caller must be within an authenticated request.");
28
- this.name = "NotAuthenticatedError";
101
+ function createCoreClient(options) {
102
+ const base = new URL(options.baseUrl);
103
+ const client2 = {};
104
+ for (const name of Object.keys(operations)) {
105
+ client2[name] = (given = {}) => call(options, base, name, given);
29
106
  }
30
- };
31
- var ConsentRequiredError = class extends Error {
32
- constructor(message = "User consent is required to access this plugin data.") {
33
- super(message);
34
- this.name = "ConsentRequiredError";
107
+ return client2;
108
+ }
109
+ async function call(options, base, name, given) {
110
+ const operation = operations[name];
111
+ let path = operation.path;
112
+ for (const [key, value] of Object.entries(given.params ?? {})) {
113
+ path = path.replace(`{${key}}`, encodeURIComponent(value));
35
114
  }
36
- };
37
- var EntitlementRequiredError = class extends Error {
38
- constructor(message = "An active entitlement is required to access this feature.") {
39
- super(message);
40
- this.name = "EntitlementRequiredError";
115
+ const url2 = new URL(path, base);
116
+ for (const [key, value] of Object.entries(given.query ?? {})) {
117
+ if (value !== void 0) url2.searchParams.set(key, String(value));
41
118
  }
42
- };
43
-
44
- // src/sessions.ts
45
- function markCurrentSessions(sessions, currentToken) {
46
- return sessions.map((s) => ({
47
- token: s.token,
48
- current: s.token === currentToken,
49
- userAgent: s.userAgent ?? null,
50
- ipAddress: s.ipAddress ?? null,
51
- createdAt: s.createdAt,
52
- updatedAt: s.updatedAt,
53
- expiresAt: s.expiresAt
54
- })).sort((a, b) => {
55
- if (a.current !== b.current) return a.current ? -1 : 1;
56
- return a.createdAt < b.createdAt ? 1 : -1;
119
+ const headers = { accept: "application/json" };
120
+ if (operation.scopes !== void 0) {
121
+ const token = typeof options.token === "function" ? await options.token() : options.token ?? void 0;
122
+ if (!token) throw new Error(`${name} needs an access token: give the client a \`token\`.`);
123
+ headers.authorization = `Bearer ${token}`;
124
+ }
125
+ let body;
126
+ if (given.body !== void 0) {
127
+ headers["content-type"] = "application/json";
128
+ body = JSON.stringify(given.body);
129
+ }
130
+ const reply = await (options.fetch ?? fetch)(url2, {
131
+ method: operation.method,
132
+ headers,
133
+ ...body === void 0 ? {} : { body }
57
134
  });
135
+ if (reply.ok) return reply.json();
136
+ throw await failure(reply);
137
+ }
138
+ async function failure(reply) {
139
+ let code = "unknown";
140
+ let message = `Core answered ${reply.status}.`;
141
+ try {
142
+ const body = await reply.json();
143
+ if (typeof body.error === "string") code = body.error;
144
+ if (typeof body.message === "string") message = body.message;
145
+ } catch {
146
+ }
147
+ const retry = Number(reply.headers.get("retry-after"));
148
+ return new CoreApiError2(
149
+ reply.status,
150
+ code,
151
+ message,
152
+ reply.status === 429 && Number.isFinite(retry) && retry > 0 ? retry : void 0
153
+ );
58
154
  }
59
155
 
60
156
  // src/auth.ts
61
- var DEFAULT_TENANT_ID = "default";
62
- var AUTH_URL = process.env.SOVEREIGN_AUTH_URL ?? `http://localhost:${process.env.AUTH_PORT ?? "3001"}`;
63
- async function getSession() {
64
- const h = await headers();
65
- const id = h.get("x-sovereign-user-id");
66
- if (!id) return null;
67
- const role = h.get("x-sovereign-user-role") ?? "platform:user";
68
- const capsRaw = h.get("x-sovereign-user-capabilities");
69
- const capabilities = capsRaw ? JSON.parse(capsRaw) : [];
70
- return {
71
- user: {
72
- id,
73
- // v1 is single-tenant, so every session belongs to the default tenant.
74
- // Multi-tenant deployments will source this from the verified session.
75
- tenantId: DEFAULT_TENANT_ID,
76
- email: h.get("x-sovereign-user-email") ?? "",
77
- name: h.get("x-sovereign-user-name") ?? null,
78
- image: h.get("x-sovereign-user-image") ?? null,
79
- role,
80
- capabilities
81
- },
82
- expiresAt: Number(h.get("x-sovereign-session-expires-at") ?? 0)
83
- };
157
+ import * as client from "openid-client";
158
+ import {
159
+ createRemoteJWKSet,
160
+ customFetch as joseFetch,
161
+ jwtVerify,
162
+ errors as joseErrors
163
+ } from "jose";
164
+
165
+ // src/session.ts
166
+ function hasRole(session, role) {
167
+ return session.roles.includes(role);
84
168
  }
85
- function hasCapability(session, capability) {
86
- return session?.user.capabilities.includes(capability) ?? false;
169
+ var cookieName = (kind, secure) => `${secure ? "__Host-" : ""}sv_${kind}`;
170
+ function setCookie(name, value, options) {
171
+ return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax${options.secure ? "; Secure" : ""}; Max-Age=${Math.max(0, Math.floor(options.maxAgeSeconds))}`;
87
172
  }
88
- async function requireSession() {
89
- const session = await getSession();
90
- if (!session) throw new NotAuthenticatedError();
91
- return session;
173
+ var clearCookie = (name, secure) => setCookie(name, "", { secure, maxAgeSeconds: 0 });
174
+ function readCookies(header) {
175
+ const found = {};
176
+ for (const part of (header ?? "").split(";")) {
177
+ const at = part.indexOf("=");
178
+ if (at < 1) continue;
179
+ const name = part.slice(0, at).trim();
180
+ if (!(name in found)) found[name] = part.slice(at + 1).trim();
181
+ }
182
+ return found;
92
183
  }
93
- async function authFetch(path, method, body) {
94
- const cookie = (await headers()).get("cookie") ?? "";
95
- return fetch(`${AUTH_URL}${path}`, {
96
- method,
97
- headers: {
98
- cookie,
99
- origin: AUTH_URL,
100
- ...body === void 0 ? {} : { "Content-Type": "application/json" }
101
- },
102
- body: body === void 0 ? void 0 : JSON.stringify(body)
103
- });
184
+ function newId() {
185
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
186
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
104
187
  }
105
- async function authError(res, action) {
106
- const body = await res.text().catch(() => "");
107
- const detail = body ? ` \u2014 ${body}` : "";
108
- return new Error(`${action} (${String(res.status)})${detail}`);
188
+ function localPath(value) {
189
+ if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\"))
190
+ return "/";
191
+ if (/[\u0000-\u001f]/.test(value)) return "/";
192
+ return value;
109
193
  }
110
- async function changePassword(input) {
111
- const res = await authFetch("/api/auth/change-password", "POST", {
112
- currentPassword: input.currentPassword,
113
- newPassword: input.newPassword,
114
- revokeOtherSessions: false
115
- });
116
- if (!res.ok) {
117
- const message = (await res.json().catch(() => null))?.message;
118
- throw new Error(message ?? `Failed to change password (${String(res.status)}).`);
194
+
195
+ // src/auth.ts
196
+ var PENDING_SECONDS = 10 * 60;
197
+ var DEFAULT_SESSION_SECONDS = 30 * 24 * 60 * 60;
198
+ var BACK_CHANNEL_EVENT = "http://schemas.openid.net/event/backchannel-logout";
199
+ var ACCOUNT_DELETED_EVENT = "urn:sovereignfs:event:account-deleted";
200
+ var SKEW_SECONDS = 60;
201
+ var Auth = class {
202
+ #o;
203
+ #secure;
204
+ #config;
205
+ constructor(options) {
206
+ this.#o = options;
207
+ this.#secure = new URL(options.appUrl).protocol === "https:";
119
208
  }
120
- }
121
- async function listSessions() {
122
- const [listRes, sessionRes] = await Promise.all([
123
- authFetch("/api/auth/list-sessions", "GET"),
124
- authFetch("/api/auth/get-session", "GET")
125
- ]);
126
- if (!listRes.ok) {
127
- throw await authError(listRes, "Failed to list sessions");
128
- }
129
- const raw = await listRes.json();
130
- const currentToken = sessionRes.ok ? (await sessionRes.json())?.session?.token ?? null : null;
131
- return markCurrentSessions(raw, currentToken);
132
- }
133
- async function revokeSession(token) {
134
- const res = await authFetch("/api/auth/revoke-session", "POST", { token });
135
- if (!res.ok) {
136
- throw await authError(res, "Failed to revoke session");
209
+ /** The app's own callback address, which must be registered with core. */
210
+ get redirectUri() {
211
+ return new URL(this.#o.redirectPath, this.#o.appUrl).toString();
137
212
  }
138
- }
139
- async function signOut() {
140
- const res = await authFetch("/api/auth/sign-out", "POST", {});
141
- if (!res.ok) {
142
- throw await authError(res, "Failed to sign out");
213
+ /** The address core sends the person to after signing out, which must be registered with core. */
214
+ get postLogoutRedirectUri() {
215
+ return new URL(this.#o.postLogoutPath, this.#o.appUrl).toString();
143
216
  }
144
- }
145
-
146
- // src/activity.ts
147
- import { headers as headers2 } from "next/headers";
148
-
149
- // src/host.ts
150
- var HOST_KEY = /* @__PURE__ */ Symbol.for("@sovereignfs/sdk:host");
151
- function holder() {
152
- return globalThis;
153
- }
154
- function provideHost(host) {
155
- holder()[HOST_KEY] = host;
156
- }
157
- function requireHost() {
158
- const host = holder()[HOST_KEY];
159
- if (!host) {
160
- throw new Error(
161
- "@sovereignfs/sdk: no runtime host is registered. SDK methods run inside the Sovereign runtime \u2014 start the platform with `pnpm dev` or `pnpm sv dev` and ensure the plugin is installed."
217
+ // ---- signing in ----------------------------------------------------------------
218
+ /** Starts a sign-in: the address to send the person to, and the cookie that names what to remember. */
219
+ async beginSignIn(options = {}) {
220
+ const config = await this.#configuration();
221
+ const verifier = client.randomPKCECodeVerifier();
222
+ const state = client.randomState();
223
+ const nonce = client.randomNonce();
224
+ const url2 = client.buildAuthorizationUrl(config, {
225
+ redirect_uri: this.redirectUri,
226
+ scope: this.#o.scopes.join(" "),
227
+ code_challenge: await client.calculatePKCECodeChallenge(verifier),
228
+ code_challenge_method: "S256",
229
+ state,
230
+ nonce
231
+ });
232
+ const id = newId();
233
+ await this.#o.sessionStore.set(
234
+ id,
235
+ { kind: "pending", data: { verifier, state, nonce, returnTo: localPath(options.returnTo) } },
236
+ this.#now() + PENDING_SECONDS * 1e3
162
237
  );
238
+ return {
239
+ url: url2.toString(),
240
+ setCookies: [
241
+ setCookie(cookieName("signin", this.#secure), id, {
242
+ secure: this.#secure,
243
+ maxAgeSeconds: PENDING_SECONDS
244
+ })
245
+ ]
246
+ };
163
247
  }
164
- return host;
165
- }
166
-
167
- // src/activity.ts
168
- var activity = {
169
- async log(entry) {
170
- const h = await headers2();
171
- const actorId = h.get("x-sovereign-user-id");
172
- const pluginId = h.get("x-sovereign-plugin-id");
173
- await requireHost().activity.log(entry, actorId, pluginId);
248
+ /** Finishes a sign-in at the callback: checks what came back, and makes the session. */
249
+ async completeSignIn(request) {
250
+ const pendingName = cookieName("signin", this.#secure);
251
+ const pendingId = readCookies(request.headers.get("cookie"))[pendingName];
252
+ const clearPending = clearCookie(pendingName, this.#secure);
253
+ if (!pendingId) throw new SignInError("No sign-in is in progress.");
254
+ const pending = await this.#o.sessionStore.get(pendingId);
255
+ await this.#o.sessionStore.delete(pendingId);
256
+ if (!pending || pending.kind !== "pending") throw new SignInError("That sign-in has run out.");
257
+ const config = await this.#configuration();
258
+ const given = new URL(request.url);
259
+ const current = new URL(`${given.pathname}${given.search}`, this.#o.appUrl);
260
+ let tokens;
261
+ try {
262
+ tokens = await client.authorizationCodeGrant(config, current, {
263
+ pkceCodeVerifier: String(pending.data.verifier),
264
+ expectedState: String(pending.data.state),
265
+ expectedNonce: String(pending.data.nonce),
266
+ idTokenExpected: true
267
+ });
268
+ } catch (error) {
269
+ throw this.#failure(error);
270
+ }
271
+ const claims = tokens.claims();
272
+ const sub = claims?.sub;
273
+ if (!claims || typeof sub !== "string") throw new SignInError("Core gave no identity.");
274
+ const sid = typeof claims.sid === "string" ? claims.sid : void 0;
275
+ const seconds = this.#o.sessionSeconds ?? DEFAULT_SESSION_SECONDS;
276
+ const expiresAt = this.#now() + seconds * 1e3;
277
+ const scopes = (tokens.scope ?? "").split(" ").filter((scope) => scope !== "");
278
+ const id = newId();
279
+ await this.#o.sessionStore.set(
280
+ id,
281
+ {
282
+ kind: "session",
283
+ sub,
284
+ ...sid ? { sid } : {},
285
+ data: { claims, scopes, idToken: tokens.id_token, expiresAt }
286
+ },
287
+ expiresAt
288
+ );
289
+ await this.#keepRefreshToken(sub, tokens.refresh_token);
290
+ return {
291
+ session: sessionFrom(id, {
292
+ kind: "session",
293
+ sub,
294
+ ...sid ? { sid } : {},
295
+ data: { claims, scopes, expiresAt }
296
+ }).session,
297
+ returnTo: localPath(String(pending.data.returnTo)),
298
+ setCookies: [
299
+ setCookie(cookieName("session", this.#secure), id, {
300
+ secure: this.#secure,
301
+ maxAgeSeconds: seconds
302
+ }),
303
+ clearPending
304
+ ]
305
+ };
174
306
  }
175
- };
176
-
177
- // src/connections.ts
178
- import { headers as headers3 } from "next/headers";
179
- var DEFAULT_TENANT_ID2 = "default";
180
- function parseCapabilities(raw) {
181
- if (!raw) return [];
182
- try {
183
- const parsed = JSON.parse(raw);
184
- return Array.isArray(parsed) ? parsed.filter((cap) => typeof cap === "string") : [];
185
- } catch {
186
- return [];
307
+ // ---- the session -----------------------------------------------------------------
308
+ /** The current session, or nothing. It makes no call to core: the session is the app's own. */
309
+ async getSession(request) {
310
+ return (await this.#find(request))?.session ?? null;
187
311
  }
188
- }
189
- async function connectionContext() {
190
- const h = await headers3();
191
- const pluginId = h.get("x-sovereign-plugin-id");
192
- if (!pluginId) {
193
- throw new Error(
194
- "sdk.connections requires a plugin route context (x-sovereign-plugin-id header missing)."
312
+ /** The current session, or `NotAuthenticatedError`. */
313
+ async requireSession(request) {
314
+ const session = await this.getSession(request);
315
+ if (!session) throw new NotAuthenticatedError();
316
+ return session;
317
+ }
318
+ /**
319
+ * Ends the app's session and clears its cookie, and gives the address of core's sign-out, which ends the person's core
320
+ * session too. It ends the app's session even if the person never returns from core.
321
+ */
322
+ async signOut(request) {
323
+ const name = cookieName("session", this.#secure);
324
+ const found = await this.#find(request);
325
+ const cookies = [clearCookie(name, this.#secure)];
326
+ if (!found) return { url: this.postLogoutRedirectUri, setCookies: cookies };
327
+ await this.#o.sessionStore.delete(found.id);
328
+ const config = await this.#configuration();
329
+ const hint = found.record.data.idToken;
330
+ if (!config.serverMetadata().end_session_endpoint || typeof hint !== "string") {
331
+ return { url: this.postLogoutRedirectUri, setCookies: cookies };
332
+ }
333
+ const url2 = client.buildEndSessionUrl(config, {
334
+ id_token_hint: hint,
335
+ post_logout_redirect_uri: this.postLogoutRedirectUri
336
+ });
337
+ return { url: url2.toString(), setCookies: cookies };
338
+ }
339
+ // ---- logout notices --------------------------------------------------------------
340
+ /** Verifies a logout token from core and ends the sessions it names (sdk §9.1). */
341
+ async handleBackChannelLogout(request) {
342
+ const refuse = () => new Response(JSON.stringify({ error: "invalid_request" }), {
343
+ status: 400,
344
+ headers: { "content-type": "application/json", "cache-control": "no-store" }
345
+ });
346
+ if (request.method !== "POST") return refuse();
347
+ let token;
348
+ try {
349
+ token = new URLSearchParams(await request.text()).get("logout_token");
350
+ } catch {
351
+ return refuse();
352
+ }
353
+ if (!token) return refuse();
354
+ const verified = await this.#verifyLogoutToken(token).catch((error) => {
355
+ if (error instanceof CoreUnavailableError) throw error;
356
+ return void 0;
357
+ });
358
+ if (!verified) return refuse();
359
+ const store = this.#o.sessionStore;
360
+ await store.set(
361
+ `seen:${verified.jti}`,
362
+ { kind: "seen", data: {} },
363
+ Math.max(verified.exp * 1e3, this.#now()) + 1e3
364
+ );
365
+ await store.deleteMatching(
366
+ verified.sid ? { sid: verified.sid } : { sub: verified.sub }
367
+ );
368
+ return new Response(null, { status: 200, headers: { "cache-control": "no-store" } });
369
+ }
370
+ /**
371
+ * Handles core's account-deleted notice (sdk §9.1, core-app §4.5): the person deleted their account, and the app must delete what it
372
+ * holds of them. The token is refused, `400` and `invalid_request`, by the rules of a logout token for its own type and event. For a
373
+ * good one the SDK ends the person's sessions and forgets their stored refresh token (when the token store can), runs `handler`, and
374
+ * answers `200` only after it has resolved. When the handler throws it answers `500` and says nothing of why, and core sends the
375
+ * notice again, so the handler must be safe to run twice.
376
+ */
377
+ async handleAccountDeleted(request, handler) {
378
+ const reply = (status, error) => new Response(error ? JSON.stringify({ error }) : null, {
379
+ status,
380
+ headers: { "content-type": "application/json", "cache-control": "no-store" }
381
+ });
382
+ if (request.method !== "POST") return reply(400, "invalid_request");
383
+ let token;
384
+ try {
385
+ token = new URLSearchParams(await request.text()).get("account_deleted_token");
386
+ } catch {
387
+ return reply(400, "invalid_request");
388
+ }
389
+ if (!token) return reply(400, "invalid_request");
390
+ const verified = await this.#verifyNotice(
391
+ token,
392
+ "account-deleted+jwt",
393
+ ACCOUNT_DELETED_EVENT
394
+ ).catch((error) => {
395
+ if (error instanceof CoreUnavailableError) throw error;
396
+ return void 0;
397
+ });
398
+ if (!verified?.sub) return reply(400, "invalid_request");
399
+ const store = this.#o.sessionStore;
400
+ await store.set(
401
+ `seen:${verified.jti}`,
402
+ { kind: "seen", data: {} },
403
+ Math.max(verified.exp * 1e3, this.#now()) + 1e3
195
404
  );
405
+ await store.deleteMatching({ sub: verified.sub });
406
+ await this.#o.tokenStore?.delete?.(verified.sub);
407
+ try {
408
+ await handler({ sub: verified.sub });
409
+ } catch {
410
+ return reply(500, "server_error");
411
+ }
412
+ return reply(200);
413
+ }
414
+ async #verifyLogoutToken(token) {
415
+ return this.#verifyNotice(token, "logout+jwt", BACK_CHANNEL_EVENT);
416
+ }
417
+ /** Checks a notice core signed (a logout token or an account-deleted token): the same rules, for its own type and event. */
418
+ async #verifyNotice(token, typ, event) {
419
+ const config = await this.#configuration();
420
+ const jwksUri = config.serverMetadata().jwks_uri;
421
+ if (!jwksUri) return void 0;
422
+ const keys = createRemoteJWKSet(new URL(jwksUri), {
423
+ [joseFetch]: (url2, init) => this.#o.transport.send(url2, init, {
424
+ operation: "jwks",
425
+ safeToRepeat: true,
426
+ deadline: this.#o.transport.deadline()
427
+ })
428
+ });
429
+ let payload;
430
+ try {
431
+ ({ payload } = await jwtVerify(token, keys, {
432
+ issuer: this.#o.coreUrl,
433
+ audience: this.#o.clientId,
434
+ typ,
435
+ currentDate: new Date(this.#now()),
436
+ requiredClaims: ["iat", "exp", "jti"]
437
+ }));
438
+ } catch (error) {
439
+ if (error instanceof CoreUnavailableError) throw error;
440
+ if (error instanceof joseErrors.JOSEError) return void 0;
441
+ if (error instanceof Error && error.name === "JWKSTimeout") {
442
+ throw new CoreUnavailableError("Core\u2019s keys could not be fetched.");
443
+ }
444
+ return void 0;
445
+ }
446
+ const now = Math.floor(this.#now() / 1e3);
447
+ if (typeof payload.iat !== "number" || payload.iat > now + SKEW_SECONDS) return void 0;
448
+ const events = payload.events;
449
+ if (!events || typeof events !== "object" || !(event in events)) return void 0;
450
+ if ("nonce" in payload) return void 0;
451
+ const sub = typeof payload.sub === "string" && payload.sub !== "" ? payload.sub : void 0;
452
+ const sid = typeof payload.sid === "string" && payload.sid !== "" ? payload.sid : void 0;
453
+ if (!sub && !sid) return void 0;
454
+ const jti = payload.jti;
455
+ if (await this.#o.sessionStore.get(`seen:${jti}`)) return void 0;
456
+ return { sub, sid, jti, exp: payload.exp };
457
+ }
458
+ // ---- inside ------------------------------------------------------------------------
459
+ async #find(request) {
460
+ const id = readCookies(request.headers.get("cookie"))[cookieName("session", this.#secure)];
461
+ if (!id) return null;
462
+ const record = await this.#o.sessionStore.get(id);
463
+ if (!record || record.kind !== "session") return null;
464
+ const { session } = sessionFrom(id, record);
465
+ if (session.expiresAt <= this.#now()) return null;
466
+ return { id, record, session };
467
+ }
468
+ /** The person's refresh token goes to the token store, so the app can call core as them (sdk §5.1). */
469
+ async #keepRefreshToken(sub, refresh) {
470
+ const store = this.#o.tokenStore;
471
+ if (!store || !refresh) return;
472
+ try {
473
+ await store.write(sub, refresh, await store.read(sub));
474
+ } catch {
475
+ }
476
+ }
477
+ #now() {
478
+ return this.#o.transport.environment.now();
196
479
  }
480
+ #configuration() {
481
+ this.#config ??= this.#discover().catch((error) => {
482
+ this.#config = void 0;
483
+ throw error;
484
+ });
485
+ return this.#config;
486
+ }
487
+ async #discover() {
488
+ const transport = this.#o.transport;
489
+ const send = (url2, options) => transport.send(url2, options, {
490
+ operation: "sign-in",
491
+ safeToRepeat: options.method === "GET",
492
+ deadline: transport.deadline()
493
+ });
494
+ const insecure = new URL(this.#o.coreUrl).protocol === "http:";
495
+ try {
496
+ const config = await client.discovery(
497
+ new URL(this.#o.coreUrl),
498
+ this.#o.clientId,
499
+ void 0,
500
+ client.ClientSecretBasic(this.#o.clientSecret),
501
+ {
502
+ ...insecure ? { execute: [client.allowInsecureRequests] } : {},
503
+ [client.customFetch]: send
504
+ }
505
+ );
506
+ config[client.customFetch] = send;
507
+ return config;
508
+ } catch (error) {
509
+ throw this.#failure(error);
510
+ }
511
+ }
512
+ /** What went wrong, as the family of errors says it: core unreachable, or a sign-in that did not check out. */
513
+ #failure(error) {
514
+ if (error instanceof CoreUnavailableError) return error;
515
+ const cause = error?.cause;
516
+ if (cause instanceof CoreUnavailableError) return cause;
517
+ return new SignInError("Core did not accept the sign-in. Start again.", { cause: error });
518
+ }
519
+ };
520
+ function sessionFrom(id, record) {
521
+ const data = record.data;
522
+ const claims = data.claims ?? {};
523
+ const roles = Array.isArray(claims.roles) ? claims.roles.filter((role) => typeof role === "string") : [];
197
524
  return {
198
- tenantId: DEFAULT_TENANT_ID2,
199
- pluginId,
200
- userId: h.get("x-sovereign-user-id"),
201
- capabilities: parseCapabilities(h.get("x-sovereign-user-capabilities"))
525
+ id,
526
+ session: {
527
+ sub: String(record.sub),
528
+ sid: record.sid,
529
+ claims,
530
+ scopes: data.scopes ?? [],
531
+ roles,
532
+ expiresAt: Number(data.expiresAt ?? 0)
533
+ }
202
534
  };
203
535
  }
204
- function requireUserForUserScope(scope, userId) {
205
- if ((scope === void 0 || scope === "user") && !userId) throw new NotAuthenticatedError();
206
- }
207
- var connections = {
208
- async create(input) {
209
- const context = await connectionContext();
210
- requireUserForUserScope(input.scope, context.userId);
211
- return requireHost().connections.create(input, context);
212
- },
213
- async list(filter) {
214
- const context = await connectionContext();
215
- requireUserForUserScope(filter?.scope, context.userId);
216
- return requireHost().connections.list(filter, context);
217
- },
218
- async get(id) {
219
- const context = await connectionContext();
220
- return requireHost().connections.get(id, context);
221
- },
222
- async update(id, input) {
223
- const context = await connectionContext();
224
- return requireHost().connections.update(id, input, context);
225
- },
226
- async disconnect(id) {
227
- const context = await connectionContext();
228
- return requireHost().connections.disconnect(id, context);
229
- },
230
- async markUsed(id) {
231
- const context = await connectionContext();
232
- return requireHost().connections.markUsed(id, context);
233
- },
234
- async markError(id, input) {
235
- const context = await connectionContext();
236
- return requireHost().connections.markError(id, input, context);
237
- },
238
- async createOAuthState(input) {
239
- const context = await connectionContext();
240
- requireUserForUserScope("user", context.userId);
241
- return requireHost().connections.createOAuthState(input, context);
242
- },
243
- async verifyOAuthState(state) {
244
- const context = await connectionContext();
245
- requireUserForUserScope("user", context.userId);
246
- return requireHost().connections.verifyOAuthState(state, context);
247
- },
248
- async getProviderConfig(provider) {
249
- const context = await connectionContext();
250
- return requireHost().connections.getProviderConfig(provider, context);
251
- }
252
- };
253
536
 
254
- // src/data.ts
255
- import { headers as headers4 } from "next/headers";
256
- var DEFAULT_TENANT_ID3 = "default";
257
- var data = {
258
- /**
259
- * Provider: register a resolver for one of the contracts this plugin exposes.
260
- * Call this before any consumer can query the contract — typically at the top
261
- * of a route handler or server component that runs when the plugin is loaded.
262
- * Registrations are in-process and reset on server restart.
263
- */
264
- provide(contract, resolver) {
265
- requireHost().data.provide(contract, resolver);
266
- },
267
- /** Consumer: read a provider plugin's contract for the current user (consent-gated). */
268
- async query(ref, params) {
269
- const h = await headers4();
270
- const consumerId = h.get("x-sovereign-plugin-id");
271
- const userId = h.get("x-sovereign-user-id");
272
- if (!userId) throw new ConsentRequiredError();
273
- const result = await requireHost().data.query(
274
- ref,
275
- consumerId,
276
- userId,
277
- DEFAULT_TENANT_ID3,
278
- params
279
- );
280
- return result;
537
+ // src/instance.ts
538
+ var FRESH_MS = 5 * 6e4;
539
+ var PHASE_ONE = ["directory"];
540
+ var API_VERSION = "v1";
541
+ var Instance = class {
542
+ #transport;
543
+ #coreUrl;
544
+ #served;
545
+ constructor(transport, coreUrl) {
546
+ this.#transport = transport;
547
+ this.#coreUrl = coreUrl;
548
+ }
549
+ /** The instance's branding, which is public (`getInstanceBranding`). */
550
+ async branding(deadline) {
551
+ const document = await this.#fetch(deadline);
552
+ this.#remember(document);
553
+ return document;
554
+ }
555
+ /** Throws `NotSupportedError` when the instance is known not to serve `capability`. */
556
+ async require(capability, deadline) {
557
+ const now = this.#transport.environment.now();
558
+ let served = this.#served;
559
+ if (!served || now - served.readAt >= FRESH_MS) {
560
+ try {
561
+ this.#remember(await this.#fetch(deadline));
562
+ } catch {
563
+ return;
564
+ }
565
+ served = this.#served;
566
+ }
567
+ if (served && !served.capabilities.has(capability)) throw new NotSupportedError(capability);
568
+ }
569
+ /** Reads the document again at the next call, since a `404` may mean the instance changed. */
570
+ forget() {
571
+ this.#served = void 0;
572
+ }
573
+ async #fetch(deadline) {
574
+ const core = createCoreClient({
575
+ baseUrl: this.#coreUrl,
576
+ fetch: (input, init) => this.#transport.send(input, init ?? {}, {
577
+ operation: "getInstanceBranding",
578
+ safeToRepeat: true,
579
+ deadline
580
+ })
581
+ });
582
+ try {
583
+ return await core.getInstanceBranding();
584
+ } catch (error) {
585
+ if (error instanceof CoreApiError2) {
586
+ throw new CoreApiError(error.status, error.code, error.message, error.retryAfter);
587
+ }
588
+ throw error;
589
+ }
590
+ }
591
+ #remember(document) {
592
+ const api = document.api;
593
+ const versions = Array.isArray(api?.versions) ? api.versions : void 0;
594
+ const listed = Array.isArray(api?.capabilities) ? api.capabilities : void 0;
595
+ const capabilities = api === void 0 || listed === void 0 ? PHASE_ONE : versions && !versions.includes(API_VERSION) ? [] : listed.filter((name) => typeof name === "string");
596
+ this.#served = {
597
+ capabilities: new Set(capabilities),
598
+ readAt: this.#transport.environment.now()
599
+ };
281
600
  }
282
601
  };
283
602
 
284
- // src/directory.ts
285
- import { headers as headers5 } from "next/headers";
286
- var DEFAULT_TENANT_ID4 = "default";
287
- var directory = {
288
- /**
289
- * Search active users in the current tenant by display name or email.
290
- * Results include only display-safe profile fields.
291
- */
292
- async searchUsers(input) {
293
- const h = await headers5();
294
- const userId = h.get("x-sovereign-user-id");
295
- if (!userId) throw new NotAuthenticatedError();
296
- return requireHost().directory.searchUsers(input, userId, DEFAULT_TENANT_ID4);
297
- },
298
- /**
299
- * Resolve explicit user IDs already stored by a plugin into display-safe
300
- * profile rows for active users.
301
- */
302
- async resolveUsers(input) {
303
- const h = await headers5();
304
- const userId = h.get("x-sovereign-user-id");
305
- if (!userId) throw new NotAuthenticatedError();
306
- return requireHost().directory.resolveUsers(input, userId, DEFAULT_TENANT_ID4);
603
+ // ../config/dist/errors.js
604
+ var ConfigError = class extends Error {
605
+ problems;
606
+ name = "ConfigError";
607
+ constructor(problems) {
608
+ const count = problems.length;
609
+ const lines = problems.map((p) => ` - ${p.variable} ${p.message}`);
610
+ super(`Invalid configuration (${count} ${count === 1 ? "problem" : "problems"}):
611
+ ${lines.join("\n")}`);
612
+ this.problems = problems;
307
613
  }
308
614
  };
309
615
 
310
- // src/db.ts
311
- var db_exports = {};
312
- __export(db_exports, {
313
- getClient: () => getClient
314
- });
315
- import { headers as headers6 } from "next/headers";
316
- async function getClient() {
317
- let pluginId = null;
318
- try {
319
- const h = await headers6();
320
- pluginId = h.get("x-sovereign-plugin-id");
321
- } catch {
616
+ // ../config/dist/define.js
617
+ var PREFIX = /^[A-Z][A-Z0-9]*$/;
618
+ var NAME = /^[A-Z][A-Z0-9_]*$/;
619
+ var FILE_SUFFIX = "_FILE";
620
+ function defineConfig(options) {
621
+ const { prefix, variables } = options;
622
+ if (!PREFIX.test(prefix)) {
623
+ throw new TypeError(`The config prefix "${prefix}" must be upper-case letters and digits, starting with a letter.`);
624
+ }
625
+ const seen = /* @__PURE__ */ new Map();
626
+ for (const [key, variable] of Object.entries(variables)) {
627
+ if (!NAME.test(variable.name)) {
628
+ throw new TypeError(`The variable name "${variable.name}" (${key}) must be upper-case letters, digits and underscores, starting with a letter.`);
629
+ }
630
+ if (variable.name.endsWith(FILE_SUFFIX)) {
631
+ throw new TypeError(`The variable name "${variable.name}" (${key}) ends in ${FILE_SUFFIX}, which is reserved for the file form of a secret.`);
632
+ }
633
+ const full = `${prefix}_${variable.name}`;
634
+ const other = seen.get(full);
635
+ if (other !== void 0)
636
+ throw new TypeError(`${full} is declared twice (${other} and ${key}).`);
637
+ seen.set(full, key);
322
638
  }
323
- return requireHost().db.getClient(pluginId);
639
+ const entries = Object.entries(variables);
640
+ return {
641
+ prefix,
642
+ load(env = process.env, loadOptions = {}) {
643
+ const readFile = loadOptions.readFile ?? defaultReadFile;
644
+ const problems = [];
645
+ const values = {};
646
+ for (const [key, variable] of entries) {
647
+ const full = `${prefix}_${variable.name}`;
648
+ const source = variable.secret ? readSecret(full, env, readFile, problems) : readPlain(full, env);
649
+ if (source === void 0)
650
+ continue;
651
+ const result = variable.resolve(source.value);
652
+ if (result.ok) {
653
+ values[key] = result.value;
654
+ continue;
655
+ }
656
+ const hint = variable.secret && source.value === void 0 ? ` (or set ${full}${FILE_SUFFIX})` : "";
657
+ problems.push({ variable: source.variable, message: `${result.message}${hint}` });
658
+ }
659
+ if (problems.length > 0)
660
+ throw new ConfigError(problems);
661
+ return values;
662
+ },
663
+ describe() {
664
+ return entries.map(([key, variable]) => {
665
+ const full = `${prefix}_${variable.name}`;
666
+ const row = {
667
+ key,
668
+ variable: full,
669
+ kind: variable.kind,
670
+ secret: variable.secret,
671
+ required: variable.required
672
+ };
673
+ if (variable.secret)
674
+ row.fileVariable = `${full}${FILE_SUFFIX}`;
675
+ if (variable.hasDefault && !variable.secret)
676
+ row.default = variable.default;
677
+ if (variable.choices)
678
+ row.choices = variable.choices;
679
+ if (variable.description !== void 0)
680
+ row.description = variable.description;
681
+ return row;
682
+ });
683
+ }
684
+ };
324
685
  }
325
-
326
- // src/e2ee.ts
327
- import { headers as headers7 } from "next/headers";
328
- var DEFAULT_TENANT_ID5 = "default";
329
- async function e2eeContext() {
330
- const h = await headers7();
331
- const userId = h.get("x-sovereign-user-id");
332
- if (!userId) throw new NotAuthenticatedError();
333
- return { tenantId: DEFAULT_TENANT_ID5, userId };
686
+ function readPlain(full, env) {
687
+ const raw = env[full]?.trim();
688
+ return { value: raw === void 0 || raw === "" ? void 0 : raw, variable: full };
334
689
  }
335
- var e2ee = {
336
- async getProfile() {
337
- const context = await e2eeContext();
338
- return requireHost().e2ee.getProfile(context);
339
- },
340
- async createProfile(input) {
341
- const context = await e2eeContext();
342
- return requireHost().e2ee.createProfile(input, context);
343
- },
344
- async getRecoveryWrapper() {
345
- const context = await e2eeContext();
346
- return requireHost().e2ee.getRecoveryWrapper(context);
347
- },
348
- async setRecoveryWrapper(input) {
349
- const context = await e2eeContext();
350
- return requireHost().e2ee.setRecoveryWrapper(input, context);
351
- },
352
- async enrollDevice(input) {
353
- const context = await e2eeContext();
354
- return requireHost().e2ee.enrollDevice(input, context);
355
- },
356
- async listDevices() {
357
- const context = await e2eeContext();
358
- return requireHost().e2ee.listDevices(context);
359
- },
360
- async revokeDevice(id) {
361
- const context = await e2eeContext();
362
- return requireHost().e2ee.revokeDevice(id, context);
690
+ function readSecret(full, env, readFile, problems) {
691
+ const fileVariable = `${full}${FILE_SUFFIX}`;
692
+ const direct = env[full];
693
+ const directSet = direct !== void 0 && direct.trim() !== "";
694
+ const path = env[fileVariable]?.trim();
695
+ const fileSet = path !== void 0 && path !== "";
696
+ if (directSet && fileSet) {
697
+ problems.push({
698
+ variable: full,
699
+ message: `is set together with ${fileVariable}; set only one`
700
+ });
701
+ return void 0;
363
702
  }
364
- };
703
+ if (fileSet) {
704
+ let content;
705
+ try {
706
+ content = readFile(path);
707
+ } catch {
708
+ problems.push({ variable: fileVariable, message: "names a file that cannot be read" });
709
+ return void 0;
710
+ }
711
+ const value = content.replace(/\r?\n$/, "");
712
+ return value.trim() === "" ? { value: void 0, variable: full } : { value, variable: fileVariable };
713
+ }
714
+ return { value: directSet ? direct : void 0, variable: full };
715
+ }
716
+ function defaultReadFile(path) {
717
+ const fs = process.getBuiltinModule("node:fs");
718
+ return fs.readFileSync(path, "utf8");
719
+ }
365
720
 
366
- // src/env.ts
367
- import { headers as headers8 } from "next/headers";
368
- var env = {
369
- async get(key) {
370
- const h = await headers8();
371
- const pluginId = h.get("x-sovereign-plugin-id");
372
- if (!pluginId) return null;
373
- const slug = pluginId.replace(/[.-]/g, "_").toUpperCase();
374
- return process.env[`SV_PLUGIN_${slug}_${key}`] ?? null;
721
+ // ../config/dist/secret.js
722
+ var REDACTED = "[redacted]";
723
+ var Secret = class {
724
+ // A private field is not an own property: Object.keys, spread, JSON and
725
+ // structuredClone never see it.
726
+ #value;
727
+ constructor(value) {
728
+ this.#value = value;
729
+ }
730
+ /** The secret itself. Call this only where it is actually used. */
731
+ reveal() {
732
+ return this.#value;
733
+ }
734
+ get length() {
735
+ return this.#value.length;
736
+ }
737
+ /** Compares without exposing either value, and without stopping at the first difference. */
738
+ equals(other) {
739
+ const a = this.#value;
740
+ const b = other.reveal();
741
+ let difference = a.length ^ b.length;
742
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
743
+ difference |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
744
+ }
745
+ return difference === 0;
746
+ }
747
+ toString() {
748
+ return REDACTED;
749
+ }
750
+ toJSON() {
751
+ return REDACTED;
752
+ }
753
+ // What `util.inspect` and `console.log` use. Symbol.for avoids importing node:util.
754
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
755
+ return `Secret(${REDACTED})`;
375
756
  }
376
757
  };
377
758
 
378
- // src/mailer.ts
379
- var mailer_exports = {};
380
- __export(mailer_exports, {
381
- send: () => send
382
- });
383
- async function send(options) {
384
- await requireHost().mailer.send(options);
759
+ // ../config/dist/variables.js
760
+ var ok = (value) => ({ ok: true, value });
761
+ var fail = (message) => ({ ok: false, message });
762
+ function make({ kind, options, secret: secret2 = false, choices, parse }) {
763
+ const required = options.required === true;
764
+ const hasDefault = options.default !== void 0;
765
+ return {
766
+ kind,
767
+ name: options.name,
768
+ secret: secret2,
769
+ required,
770
+ description: options.description,
771
+ hasDefault,
772
+ default: secret2 ? void 0 : options.default,
773
+ choices,
774
+ resolve(raw) {
775
+ if (raw === void 0) {
776
+ if (required)
777
+ return fail("is required but not set");
778
+ return ok(options.default);
779
+ }
780
+ return parse(raw);
781
+ }
782
+ };
385
783
  }
386
-
387
- // src/notifications.ts
388
- function getPluginId(headers13) {
389
- return headers13.get("x-sovereign-plugin-id");
784
+ function string(options) {
785
+ return make({ kind: "string", options, parse: (raw) => ok(raw) });
390
786
  }
391
- var notifications = {
392
- /**
393
- * Send a notification to a user.
394
- *
395
- * The `source` and `sourceType` fields are set by the runtime from the
396
- * calling plugin's ID (injected via `x-sovereign-plugin-id` header) — plugins
397
- * cannot forge sender identity.
398
- *
399
- * @example
400
- * ```ts
401
- * await sdk.notifications.send({
402
- * recipientUserId: userId,
403
- * title: 'Your export is ready',
404
- * url: '/myPlugin/exports',
405
- * category: 'info',
406
- * });
407
- * ```
408
- */
409
- async send(input, requestHeaders) {
410
- const pluginId = requestHeaders ? getPluginId(requestHeaders) ?? "unknown" : "unknown";
411
- return requireHost().notifications.send(input, pluginId);
787
+ function url(options) {
788
+ const protocols = options.protocols ?? ["http:", "https:"];
789
+ return make({
790
+ kind: "url",
791
+ options,
792
+ parse(raw) {
793
+ const parsed = parseUrl(raw);
794
+ if (!parsed)
795
+ return fail("is not a valid URL");
796
+ if (!protocols.includes(parsed.protocol))
797
+ return fail(`must use ${protocols.join(" or ")}`);
798
+ if (parsed.username || parsed.password) {
799
+ return fail("must not contain a username or password; use a secret for that");
800
+ }
801
+ if (options.origin) {
802
+ if (parsed.pathname !== "/" && parsed.pathname !== "" || parsed.search || parsed.hash) {
803
+ return fail("must be an address with no path, query or fragment");
804
+ }
805
+ return ok(parsed.origin);
806
+ }
807
+ return ok(parsed.origin + trimTrailingSlashes(parsed.pathname) + parsed.search + parsed.hash);
808
+ }
809
+ });
810
+ }
811
+ function trimTrailingSlashes(value) {
812
+ let end = value.length;
813
+ while (end > 0 && value[end - 1] === "/")
814
+ end -= 1;
815
+ return value.slice(0, end);
816
+ }
817
+ function parseUrl(raw) {
818
+ try {
819
+ return new URL(raw);
820
+ } catch {
821
+ return void 0;
412
822
  }
413
- };
414
-
415
- // src/platform.ts
416
- var platform_exports = {};
417
- __export(platform_exports, {
418
- getConfig: () => getConfig
419
- });
420
- async function getConfig() {
421
- return requireHost().platform.getConfig();
823
+ }
824
+ function secret(options) {
825
+ return make({
826
+ kind: "secret",
827
+ options,
828
+ secret: true,
829
+ parse(raw) {
830
+ if (options.minLength !== void 0 && raw.length < options.minLength) {
831
+ return fail(`must be at least ${options.minLength} characters`);
832
+ }
833
+ if (options.url) {
834
+ const parsed = parseUrl(raw);
835
+ if (!parsed)
836
+ return fail("is not a valid URL");
837
+ if (!options.url.protocols.includes(parsed.protocol)) {
838
+ return fail(`must use ${options.url.protocols.join(" or ")}`);
839
+ }
840
+ }
841
+ return ok(new Secret(raw));
842
+ }
843
+ });
422
844
  }
423
845
 
424
- // src/plugins.ts
425
- import { headers as headers9 } from "next/headers";
426
- function parseCapabilities2(raw) {
427
- if (!raw) return [];
846
+ // src/settings.ts
847
+ var hasPrefix = (source) => "prefix" in source;
848
+ function readSettings(source) {
849
+ if (hasPrefix(source)) return fromPrefix(source);
850
+ return fromOptions(source);
851
+ }
852
+ function fromPrefix(source) {
853
+ let config;
428
854
  try {
429
- const parsed = JSON.parse(raw);
430
- return Array.isArray(parsed) ? parsed.filter((cap) => typeof cap === "string") : [];
431
- } catch {
432
- return [];
855
+ config = defineConfig({
856
+ prefix: source.prefix,
857
+ variables: {
858
+ appUrl: url({ name: "URL", required: true }),
859
+ coreUrl: url({ name: "CORE_URL", required: true, origin: true }),
860
+ clientId: string({ name: "CORE_CLIENT_ID", required: true }),
861
+ clientSecret: secret({ name: "CORE_CLIENT_SECRET", required: true })
862
+ }
863
+ });
864
+ } catch (error) {
865
+ throw new ConfigurationError(
866
+ error instanceof Error ? error.message : "The prefix cannot be used."
867
+ );
868
+ }
869
+ try {
870
+ const env = source.env ?? globalThis.process?.env ?? {};
871
+ const values = config.load(env, source.readFile ? { readFile: source.readFile } : {});
872
+ return checked({
873
+ appUrl: values.appUrl,
874
+ coreUrl: values.coreUrl,
875
+ clientId: values.clientId,
876
+ clientSecret: values.clientSecret.reveal()
877
+ });
878
+ } catch (error) {
879
+ if (error instanceof ConfigError) throw new ConfigurationError(error.message);
880
+ throw error;
433
881
  }
434
882
  }
435
- async function requestContext() {
436
- const h = await headers9();
437
- return {
438
- userId: h.get("x-sovereign-user-id"),
439
- capabilities: parseCapabilities2(h.get("x-sovereign-user-capabilities"))
883
+ function fromOptions(source) {
884
+ const given = {
885
+ appUrl: source.appUrl,
886
+ coreUrl: source.coreUrl,
887
+ clientId: source.clientId,
888
+ clientSecret: source.clientSecret
889
+ };
890
+ const problems = [];
891
+ for (const [name, value] of Object.entries(given)) {
892
+ if (typeof value !== "string" || value.trim() === "") problems.push(`${name} is required`);
893
+ }
894
+ if (problems.length > 0) throw problemsOf(problems);
895
+ return checked(given);
896
+ }
897
+ function checked(settings) {
898
+ const problems = [];
899
+ const origin = (name, value, bare) => {
900
+ try {
901
+ const parsed = new URL(value);
902
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
903
+ problems.push(`${name} must be an http or https address`);
904
+ } else if (bare && (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "")) {
905
+ problems.push(`${name} must have no path: it is the instance's own address`);
906
+ }
907
+ return bare ? parsed.origin : parsed.toString();
908
+ } catch {
909
+ problems.push(`${name} is not an address`);
910
+ return value;
911
+ }
440
912
  };
913
+ const appUrl = origin("the app address", settings.appUrl, false);
914
+ const coreUrl = origin("the core address", settings.coreUrl, true);
915
+ if (settings.clientId.trim() === "") problems.push("the client id is required");
916
+ if (settings.clientSecret.trim() === "") problems.push("the client secret is required");
917
+ if (problems.length > 0) throw problemsOf(problems);
918
+ return { appUrl, coreUrl, clientId: settings.clientId, clientSecret: settings.clientSecret };
441
919
  }
442
- var plugins = {
443
- /** Discover one installed plugin's status. `null` when not installed. */
444
- async get(id) {
445
- const { userId, capabilities } = await requestContext();
446
- return requireHost().plugins.get(id, userId, capabilities);
447
- },
448
- /** Discover installed plugins, optionally filtered to those providing a contract. */
449
- async list(filter) {
450
- const { userId, capabilities } = await requestContext();
451
- return requireHost().plugins.list(filter, userId, capabilities);
452
- },
453
- /** Whether the current user has granted this plugin's requested data contract. */
454
- async getConsentStatus(ref) {
455
- const h = await headers9();
456
- const consumerId = h.get("x-sovereign-plugin-id");
457
- const userId = h.get("x-sovereign-user-id");
458
- if (!consumerId || !userId) return "not_granted";
459
- return requireHost().plugins.getConsentStatus(ref, consumerId, userId);
920
+ function problemsOf(problems) {
921
+ return new ConfigurationError(
922
+ `The Sovereign client is configured wrongly (${problems.length} ${problems.length === 1 ? "problem" : "problems"}):
923
+ ${problems.map((problem) => ` - ${problem}`).join("\n")}`
924
+ );
925
+ }
926
+
927
+ // src/transport.ts
928
+ var SYSTEM = {
929
+ now: () => Date.now(),
930
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
931
+ random: () => Math.random()
932
+ };
933
+ var DEFAULT_TIMEOUT_MS = 5e3;
934
+ var OVERALL_LIMIT_MS = 1e4;
935
+ var RETRIES = 2;
936
+ var BACKOFF_MS = 200;
937
+ var Transport = class {
938
+ #fetch;
939
+ #timeoutMs;
940
+ #logger;
941
+ #env;
942
+ constructor(options = {}) {
943
+ this.#fetch = options.fetch ?? ((input, init) => fetch(input, init));
944
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
945
+ this.#logger = options.logger;
946
+ this.#env = options.environment ?? SYSTEM;
947
+ }
948
+ get environment() {
949
+ return this.#env;
950
+ }
951
+ get timeoutMs() {
952
+ return this.#timeoutMs;
953
+ }
954
+ /** A deadline for a call that starts now. */
955
+ deadline() {
956
+ return this.#env.now() + OVERALL_LIMIT_MS;
957
+ }
958
+ /** `fetch` for one request of an operation, retrying as the specification says. */
959
+ async send(input, init, meta) {
960
+ let tries = 0;
961
+ let waited429 = false;
962
+ for (; ; ) {
963
+ const started = this.#env.now();
964
+ const remaining = meta.deadline - started;
965
+ if (remaining <= 0) {
966
+ throw new CoreUnavailableError("Core did not answer in time.", { requestSent: false });
967
+ }
968
+ let unavailable;
969
+ try {
970
+ const reply = await this.#fetch(input, {
971
+ ...init,
972
+ signal: AbortSignal.timeout(Math.min(this.#timeoutMs, remaining))
973
+ });
974
+ this.#log(meta.operation, reply.status, started);
975
+ if (reply.status === 429 && !waited429) {
976
+ const wait = secondsOf(reply.headers.get("retry-after")) * 1e3;
977
+ if (wait > 0 && this.#env.now() + wait < meta.deadline) {
978
+ waited429 = true;
979
+ await reply.body?.cancel().catch(() => void 0);
980
+ await this.#env.sleep(wait);
981
+ continue;
982
+ }
983
+ }
984
+ if (reply.status < 500) return reply;
985
+ await reply.body?.cancel().catch(() => void 0);
986
+ unavailable = new CoreUnavailableError(`Core answered ${reply.status}.`, {
987
+ status: reply.status
988
+ });
989
+ } catch (error) {
990
+ if (error instanceof CoreUnavailableError) throw error;
991
+ this.#log(meta.operation, "error", started);
992
+ unavailable = new CoreUnavailableError(describe(error), {
993
+ cause: error,
994
+ requestSent: !neverSent(error)
995
+ });
996
+ }
997
+ if (!meta.safeToRepeat || tries >= RETRIES) throw unavailable;
998
+ const pause = BACKOFF_MS * 2 ** tries * (0.5 + this.#env.random());
999
+ if (this.#env.now() + pause >= meta.deadline) throw unavailable;
1000
+ tries += 1;
1001
+ await this.#env.sleep(pause);
1002
+ }
1003
+ }
1004
+ #log(operation, status, started) {
1005
+ if (!this.#logger) return;
1006
+ try {
1007
+ this.#logger({ operation, status, durationMs: Math.max(0, this.#env.now() - started) });
1008
+ } catch {
1009
+ }
460
1010
  }
461
1011
  };
1012
+ function secondsOf(header) {
1013
+ const seconds = Number(header);
1014
+ return header !== null && Number.isFinite(seconds) && seconds > 0 ? seconds : 0;
1015
+ }
1016
+ function describe(error) {
1017
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
1018
+ return "Core did not answer in time.";
1019
+ }
1020
+ return "Core could not be reached.";
1021
+ }
1022
+ var NEVER_SENT = /* @__PURE__ */ new Set([
1023
+ "ECONNREFUSED",
1024
+ "ENOTFOUND",
1025
+ "EAI_AGAIN",
1026
+ "EHOSTUNREACH",
1027
+ "ENETUNREACH",
1028
+ "UND_ERR_CONNECT_TIMEOUT"
1029
+ ]);
1030
+ function neverSent(error) {
1031
+ for (let current = error, depth = 0; current && depth < 4; depth += 1) {
1032
+ const code = current.code;
1033
+ if (typeof code === "string" && NEVER_SENT.has(code)) return true;
1034
+ current = current.cause;
1035
+ }
1036
+ return false;
1037
+ }
462
1038
 
463
- // src/portability.ts
464
- import { headers as headers10 } from "next/headers";
465
- var portability = {
466
- /** Provider: register this plugin's export resolver. */
467
- async provideExport(resolver) {
468
- const pluginId = (await headers10()).get("x-sovereign-plugin-id");
469
- if (!pluginId) {
470
- throw new Error(
471
- "sdk.portability.provideExport() must be called from a plugin route context (x-sovereign-plugin-id header missing)."
1039
+ // src/tokens.ts
1040
+ var EARLY_MS = 3e4;
1041
+ var keyOf = (actor) => actor.kind === "person" ? `person:${actor.sub}` : "app";
1042
+ var TokenManager = class {
1043
+ #options;
1044
+ #cache = /* @__PURE__ */ new Map();
1045
+ #flights = /* @__PURE__ */ new Map();
1046
+ /** Refresh tokens that core has probably rotated away, by person: sending one again would look like theft. */
1047
+ #lost = /* @__PURE__ */ new Map();
1048
+ #endpoint;
1049
+ constructor(options) {
1050
+ this.#options = options;
1051
+ }
1052
+ /** An access token for the Core API for this actor, from the cache when it is good. */
1053
+ token(actor, deadline) {
1054
+ const key = keyOf(actor);
1055
+ const cached = this.#cache.get(key);
1056
+ if (cached && cached.expiresAt - EARLY_MS > this.#options.transport.environment.now()) {
1057
+ return Promise.resolve(cached.token);
1058
+ }
1059
+ const running = this.#flights.get(key);
1060
+ if (running) return running;
1061
+ const flight = (actor.kind === "person" ? this.#refresh(actor.sub, deadline) : this.#appToken(deadline)).finally(() => this.#flights.delete(key));
1062
+ this.#flights.set(key, flight);
1063
+ return flight;
1064
+ }
1065
+ /** Forgets the cached token of an actor, after core said it is no good. */
1066
+ discard(actor) {
1067
+ this.#cache.delete(keyOf(actor));
1068
+ }
1069
+ /** The address of core's token endpoint, from its discovery document, asked for once. */
1070
+ #tokenEndpoint(deadline) {
1071
+ this.#endpoint ??= this.#discover(deadline).catch((error) => {
1072
+ this.#endpoint = void 0;
1073
+ throw error;
1074
+ });
1075
+ return this.#endpoint;
1076
+ }
1077
+ async #discover(deadline) {
1078
+ const reply = await this.#options.transport.send(
1079
+ `${this.#options.coreUrl}/.well-known/openid-configuration`,
1080
+ { method: "GET", headers: { accept: "application/json" } },
1081
+ { operation: "discovery", safeToRepeat: true, deadline }
1082
+ );
1083
+ if (!reply.ok) throw new CoreUnavailableError(`Core answered ${reply.status}.`);
1084
+ const document = await reply.json().catch(() => void 0);
1085
+ if (typeof document?.token_endpoint !== "string") {
1086
+ throw new CoreUnavailableError("Core published no token endpoint.");
1087
+ }
1088
+ return document.token_endpoint;
1089
+ }
1090
+ #request(body, endpoint, deadline, safe) {
1091
+ const { clientId, clientSecret, coreUrl, transport } = this.#options;
1092
+ const basic = btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`);
1093
+ return transport.send(
1094
+ endpoint,
1095
+ {
1096
+ method: "POST",
1097
+ headers: {
1098
+ accept: "application/json",
1099
+ "content-type": "application/x-www-form-urlencoded",
1100
+ authorization: `Basic ${basic}`
1101
+ },
1102
+ body: new URLSearchParams({ ...body, resource: `${coreUrl}/api/v1` }).toString()
1103
+ },
1104
+ { operation: "token", safeToRepeat: safe, deadline }
1105
+ );
1106
+ }
1107
+ // ---- a person ----------------------------------------------------------------
1108
+ #refresh(sub, deadline) {
1109
+ const store = this.#options.store;
1110
+ if (!store) {
1111
+ return Promise.reject(
1112
+ new SessionEndedError("No token store was given, so there is no refresh token to use.")
472
1113
  );
473
1114
  }
474
- requireHost().portability.provideExport(pluginId, resolver);
475
- },
476
- /** Provider: register this plugin's import handler. */
477
- async provideImport(handler) {
478
- const pluginId = (await headers10()).get("x-sovereign-plugin-id");
479
- if (!pluginId) {
480
- throw new Error(
481
- "sdk.portability.provideImport() must be called from a plugin route context (x-sovereign-plugin-id header missing)."
1115
+ const work = () => this.#refreshOnce(store, sub, deadline);
1116
+ return store.lock ? store.lock(sub, work) : work();
1117
+ }
1118
+ async #refreshOnce(store, sub, deadline) {
1119
+ const previous = await store.read(sub);
1120
+ if (!previous)
1121
+ throw new SessionEndedError("This person has no refresh token. They must sign in again.");
1122
+ if (this.#lost.get(sub) === previous) {
1123
+ throw new SessionEndedError(
1124
+ "This person\u2019s session was lost in a renewal. They must sign in again."
482
1125
  );
483
1126
  }
484
- requireHost().portability.provideImport(pluginId, handler);
485
- },
486
- /** Provider: register this plugin's deletion handler (RFC 0033). */
487
- async provideDelete(handler) {
488
- const pluginId = (await headers10()).get("x-sovereign-plugin-id");
489
- if (!pluginId) {
490
- throw new Error(
491
- "sdk.portability.provideDelete() must be called from a plugin route context (x-sovereign-plugin-id header missing)."
1127
+ const endpoint = await this.#tokenEndpoint(deadline);
1128
+ let reply;
1129
+ try {
1130
+ reply = await this.#request(
1131
+ { grant_type: "refresh_token", refresh_token: previous },
1132
+ endpoint,
1133
+ deadline,
1134
+ false
1135
+ );
1136
+ } catch (error) {
1137
+ if (error instanceof CoreUnavailableError && (!error.requestSent || error.status !== void 0)) {
1138
+ throw error;
1139
+ }
1140
+ this.#lost.set(sub, previous);
1141
+ throw new SessionEndedError(
1142
+ "Core did not answer the request to renew this person\u2019s session, so it may have been renewed. They must sign in again."
1143
+ );
1144
+ }
1145
+ if (!reply.ok) throw await refusal(reply, true);
1146
+ const answer = await reply.json().catch(() => void 0);
1147
+ const access = answer?.access_token;
1148
+ const lifetime = Number(answer?.expires_in);
1149
+ if (typeof access !== "string" || !Number.isFinite(lifetime)) {
1150
+ throw new SessionEndedError(
1151
+ "Core\u2019s answer to a renewal could not be read. They must sign in again."
1152
+ );
1153
+ }
1154
+ const next = typeof answer?.refresh_token === "string" ? answer.refresh_token : previous;
1155
+ if (next !== previous) {
1156
+ const stored = await (async () => store.write(sub, next, previous))().then(
1157
+ (result) => result === true,
1158
+ () => false
492
1159
  );
1160
+ if (!stored) {
1161
+ this.#lost.set(sub, previous);
1162
+ throw new SessionEndedError(
1163
+ "The renewed token could not be stored. They must sign in again."
1164
+ );
1165
+ }
493
1166
  }
494
- requireHost().portability.provideDelete(pluginId, handler);
1167
+ this.#cache.set(keyOf({ kind: "person", sub }), {
1168
+ token: access,
1169
+ expiresAt: this.#options.transport.environment.now() + lifetime * 1e3
1170
+ });
1171
+ return access;
1172
+ }
1173
+ // ---- the app -----------------------------------------------------------------
1174
+ async #appToken(deadline) {
1175
+ const endpoint = await this.#tokenEndpoint(deadline);
1176
+ const reply = await this.#request(
1177
+ { grant_type: "client_credentials" },
1178
+ endpoint,
1179
+ deadline,
1180
+ true
1181
+ );
1182
+ if (!reply.ok) throw await refusal(reply, false);
1183
+ const answer = await reply.json().catch(() => void 0);
1184
+ const access = answer?.access_token;
1185
+ const lifetime = Number(answer?.expires_in);
1186
+ if (typeof access !== "string" || !Number.isFinite(lifetime)) {
1187
+ throw new CoreUnavailableError(
1188
+ "Core\u2019s answer to a request for an app token could not be read."
1189
+ );
1190
+ }
1191
+ this.#cache.set("app", {
1192
+ token: access,
1193
+ expiresAt: this.#options.transport.environment.now() + lifetime * 1e3
1194
+ });
1195
+ return access;
495
1196
  }
496
1197
  };
497
-
498
- // src/secrets.ts
499
- import { headers as headers11 } from "next/headers";
500
- var DEFAULT_TENANT_ID6 = "default";
501
- function parseCapabilities3(raw) {
502
- if (!raw) return [];
1198
+ async function refusal(reply, person) {
1199
+ let code = "unknown";
1200
+ let message = `Core answered ${reply.status}.`;
503
1201
  try {
504
- const parsed = JSON.parse(raw);
505
- return Array.isArray(parsed) ? parsed.filter((cap) => typeof cap === "string") : [];
1202
+ const body = await reply.json();
1203
+ if (typeof body.error === "string") code = body.error;
1204
+ if (typeof body.error_description === "string") message = body.error_description;
506
1205
  } catch {
507
- return [];
508
1206
  }
509
- }
510
- async function secretContext() {
511
- const h = await headers11();
512
- const pluginId = h.get("x-sovereign-plugin-id");
513
- if (!pluginId) {
514
- throw new Error(
515
- "sdk.secrets requires a plugin route context (x-sovereign-plugin-id header missing)."
1207
+ if (person && code === "invalid_grant") {
1208
+ return new SessionEndedError(
1209
+ "This person\u2019s session cannot be renewed. They must sign in again."
516
1210
  );
517
1211
  }
518
- return {
519
- tenantId: DEFAULT_TENANT_ID6,
520
- pluginId,
521
- userId: h.get("x-sovereign-user-id"),
522
- capabilities: parseCapabilities3(h.get("x-sovereign-user-capabilities"))
523
- };
524
- }
525
- function requireUserForUserScope2(scope, userId) {
526
- if (scope === "user" && !userId) throw new NotAuthenticatedError();
1212
+ const retryAfter = secondsOf(reply.headers.get("retry-after"));
1213
+ return new CoreApiError(reply.status, code, message, retryAfter > 0 ? retryAfter : void 0);
527
1214
  }
528
- var secrets = {
529
- async create(input) {
530
- const context = await secretContext();
531
- requireUserForUserScope2(input.scope, context.userId);
532
- return requireHost().secrets.create(input, context);
533
- },
534
- async get(id) {
535
- const context = await secretContext();
536
- return requireHost().secrets.get(id, context);
537
- },
538
- async list(scope) {
539
- const context = await secretContext();
540
- if (scope) requireUserForUserScope2(scope, context.userId);
541
- return requireHost().secrets.list(scope, context);
542
- },
543
- async update(id, value) {
544
- const context = await secretContext();
545
- return requireHost().secrets.update(id, value, context);
546
- },
547
- async delete(id) {
548
- const context = await secretContext();
549
- return requireHost().secrets.delete(id, context);
550
- }
551
- };
552
1215
 
553
- // src/storage.ts
554
- import { headers as headers12 } from "next/headers";
555
- var DEFAULT_TENANT_ID7 = "default";
556
- async function storageContext() {
557
- const h = await headers12();
558
- const pluginId = h.get("x-sovereign-plugin-id");
559
- if (!pluginId) {
560
- throw new Error(
561
- "sdk.storage requires a plugin route context (x-sovereign-plugin-id header missing)."
562
- );
1216
+ // src/sovereign.ts
1217
+ var CAPABILITY = {
1218
+ searchDirectoryUsers: "directory",
1219
+ resolveDirectoryUsers: "directory",
1220
+ findDirectoryUserByEmail: "directory",
1221
+ sendNotification: "notifications",
1222
+ sendEmail: "email",
1223
+ logActivity: "activity",
1224
+ listNotifications: "notifications",
1225
+ markNotificationRead: "notifications",
1226
+ markAllNotificationsRead: "notifications",
1227
+ dismissNotification: "notifications",
1228
+ dismissAllNotifications: "notifications"
1229
+ };
1230
+ function createSovereign(options) {
1231
+ const settings = readSettings(options);
1232
+ const transport = new Transport({
1233
+ ...options.fetch ? { fetch: options.fetch } : {},
1234
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
1235
+ ...options.logger ? { logger: options.logger } : {},
1236
+ ...options.environment ? { environment: options.environment } : {}
1237
+ });
1238
+ const tokens = new TokenManager({
1239
+ transport,
1240
+ coreUrl: settings.coreUrl,
1241
+ clientId: settings.clientId,
1242
+ clientSecret: settings.clientSecret,
1243
+ store: options.tokenStore
1244
+ });
1245
+ const instance = new Instance(transport, settings.coreUrl);
1246
+ let auth;
1247
+ const pageOf = (path) => new URL(path, settings.coreUrl).toString();
1248
+ async function execute(actor, name, request) {
1249
+ const deadline = transport.deadline();
1250
+ const operation = operations[name];
1251
+ const capability = CAPABILITY[name];
1252
+ if (capability) await instance.require(capability, deadline);
1253
+ for (let attempt = 0; ; attempt += 1) {
1254
+ const token = actor ? await tokens.token(actor, deadline) : void 0;
1255
+ const core = createCoreClient({
1256
+ baseUrl: settings.coreUrl,
1257
+ ...token ? { token } : {},
1258
+ fetch: (input, init) => transport.send(input, init ?? {}, {
1259
+ operation: name,
1260
+ safeToRepeat: operation.safeToRepeat,
1261
+ deadline
1262
+ })
1263
+ });
1264
+ try {
1265
+ const call2 = core[name];
1266
+ return await call2(request);
1267
+ } catch (error) {
1268
+ if (!(error instanceof CoreApiError2)) throw error;
1269
+ if (error.status === 404) instance.forget();
1270
+ if (error.status === 401 && actor) {
1271
+ tokens.discard(actor);
1272
+ if (attempt === 0) continue;
1273
+ if (actor.kind === "person") {
1274
+ throw new SessionEndedError(
1275
+ "Core refused this person\u2019s token twice. They must sign in again."
1276
+ );
1277
+ }
1278
+ }
1279
+ throw new CoreApiError(error.status, error.code, error.message, error.retryAfter);
1280
+ }
1281
+ }
563
1282
  }
1283
+ const directory = (actor) => ({
1284
+ searchUsers: (query) => execute(actor, "searchDirectoryUsers", { query }),
1285
+ resolveUsers: (body) => execute(actor, "resolveDirectoryUsers", { body }),
1286
+ findUserByEmail: (query) => execute(actor, "findDirectoryUserByEmail", { query })
1287
+ });
1288
+ const notifications = (actor) => ({
1289
+ list: (query) => execute(actor, "listNotifications", query === void 0 ? {} : { query }),
1290
+ markRead: (id) => execute(actor, "markNotificationRead", { params: { id } }),
1291
+ markAllRead: () => execute(actor, "markAllNotificationsRead", void 0),
1292
+ dismiss: (id) => execute(actor, "dismissNotification", { params: { id } }),
1293
+ dismissAll: () => execute(actor, "dismissAllNotifications", void 0)
1294
+ });
564
1295
  return {
565
- tenantId: DEFAULT_TENANT_ID7,
566
- pluginId,
567
- userId: h.get("x-sovereign-user-id")
1296
+ as(person) {
1297
+ if (typeof person?.sub !== "string" || person.sub === "") {
1298
+ throw new ConfigurationError("A person needs a sub: pass the session or { sub }.");
1299
+ }
1300
+ if (!options.tokenStore) {
1301
+ throw new ConfigurationError(
1302
+ "Calling as a person needs a token store: pass tokenStore when creating the client."
1303
+ );
1304
+ }
1305
+ const actor = { kind: "person", sub: person.sub };
1306
+ return { directory: directory(actor), notifications: notifications(actor) };
1307
+ },
1308
+ app: {
1309
+ notifications: {
1310
+ send: (message) => execute({ kind: "app" }, "sendNotification", { body: message })
1311
+ },
1312
+ email: {
1313
+ sendToUser: (message) => execute({ kind: "app" }, "sendEmail", { body: message })
1314
+ },
1315
+ activity: {
1316
+ log: (entry) => execute({ kind: "app" }, "logActivity", { body: entry })
1317
+ }
1318
+ },
1319
+ get auth() {
1320
+ if (!options.sessionStore) {
1321
+ throw new ConfigurationError(
1322
+ "Signing people in needs a session store: pass sessionStore when creating the client."
1323
+ );
1324
+ }
1325
+ auth ??= new Auth({
1326
+ appUrl: settings.appUrl,
1327
+ coreUrl: settings.coreUrl,
1328
+ clientId: settings.clientId,
1329
+ clientSecret: settings.clientSecret,
1330
+ transport,
1331
+ sessionStore: options.sessionStore,
1332
+ tokenStore: options.tokenStore,
1333
+ scopes: options.scopes ?? ["openid", "profile", "email", "offline_access"],
1334
+ redirectPath: options.redirectPath ?? "/auth/callback",
1335
+ postLogoutPath: options.postLogoutPath ?? "/",
1336
+ sessionSeconds: options.sessionSeconds
1337
+ });
1338
+ return auth;
1339
+ },
1340
+ links: {
1341
+ profile: pageOf("/profile"),
1342
+ security: pageOf("/security"),
1343
+ preferences: pageOf("/preferences"),
1344
+ connectedApps: pageOf("/connected-apps"),
1345
+ deleteAccount: pageOf("/delete-account")
1346
+ },
1347
+ instance: {
1348
+ getBranding: () => instance.branding(transport.deadline())
1349
+ }
568
1350
  };
569
1351
  }
570
- var storage = {
571
- async put(input) {
572
- const context = await storageContext();
573
- return requireHost().storage.put(input, context);
574
- },
575
- async get(key) {
576
- const context = await storageContext();
577
- return requireHost().storage.get(key, context);
578
- },
579
- async delete(key) {
580
- const context = await storageContext();
581
- return requireHost().storage.delete(key, context);
582
- },
583
- async list(prefix) {
584
- const context = await storageContext();
585
- return requireHost().storage.list(prefix, context);
586
- },
587
- /** Create a short-lived, read-only download URL for an object (default 5 minutes, max 1 hour). */
588
- async getSignedUrl(key, options) {
589
- const context = await storageContext();
590
- return requireHost().storage.getSignedUrl(key, options, context);
591
- }
592
- };
593
-
594
- // src/unimplemented.ts
595
- var events = {
596
- publish(_event, _payload) {
597
- throw new NotImplementedError("sdk.events.publish() is not implemented in Sovereign v1.");
598
- },
599
- subscribe(_event, _handler) {
600
- throw new NotImplementedError("sdk.events.subscribe() is not implemented in Sovereign v1.");
601
- }
602
- };
603
- var billing = {
604
- /**
605
- * Returns the current user's active entitlement for the calling plugin, or
606
- * `null` when no entitlement exists. Use this to branch UI by tier.
607
- */
608
- getEntitlement(_headers) {
609
- throw new NotImplementedError("sdk.billing.getEntitlement() is not yet implemented.");
610
- },
611
- /**
612
- * Asserts that the current user has an active entitlement for the calling
613
- * plugin. Throws `EntitlementRequiredError` when absent. Use for in-plugin
614
- * feature gating (route-level access is already gated by the middleware).
615
- */
616
- requireEntitlement(_headers) {
617
- throw new EntitlementRequiredError(
618
- "sdk.billing.requireEntitlement() is not yet implemented \u2014 no entitlement check performed."
619
- );
620
- }
621
- };
622
-
623
- // src/index.ts
624
- var sdk = {
625
- // Stable (v1.0.0).
626
- auth: auth_exports,
627
- db: db_exports,
628
- mailer: mailer_exports,
629
- platform: platform_exports,
630
- // Experimental / reserved — shape may change; some throw until implemented.
631
- data,
632
- storage,
633
- directory,
634
- notifications,
635
- secrets,
636
- connections,
637
- e2ee,
638
- events,
639
- activity,
640
- portability,
641
- plugins,
642
- env,
643
- billing
644
- };
645
1352
  export {
646
- ConsentRequiredError,
647
- EntitlementRequiredError,
1353
+ ConfigurationError,
1354
+ CoreApiError,
1355
+ CoreUnavailableError,
648
1356
  NotAuthenticatedError,
649
- NotImplementedError,
650
- provideHost,
651
- sdk
1357
+ NotSupportedError,
1358
+ SessionEndedError,
1359
+ SignInError,
1360
+ SovereignError,
1361
+ createSovereign,
1362
+ hasRole
652
1363
  };