@sovereignfs/sdk 1.25.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -661
- package/README.md +97 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +4 -0
- package/dist/chunk-NYL4LLXF.js +65 -0
- package/dist/index.d.ts +57 -529
- package/dist/index.js +1283 -572
- package/dist/next.d.ts +37 -0
- package/dist/next.js +68 -0
- package/dist/session-DJl2XYMc.d.ts +44 -0
- package/dist/sovereign-CazGKEVc.d.ts +517 -0
- package/dist/testing.d.ts +181 -0
- package/dist/testing.js +560 -0
- package/package.json +53 -27
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-NQLGPAL6.js +0 -62
- package/dist/e2ee-crypto.d.ts +0 -84
- package/dist/e2ee-crypto.js +0 -162
- package/dist/e2ee-device.d.ts +0 -21
- package/dist/e2ee-device.js +0 -13
- package/dist/e2ee-object.d.ts +0 -50
- package/dist/e2ee-object.js +0 -75
- package/dist/e2ee-state-U-GEJ7oL.d.ts +0 -411
- package/dist/e2ee-state.d.ts +0 -1
- package/dist/e2ee-state.js +0 -36
- package/src/__tests__/e2ee-crypto.test.ts +0 -156
- package/src/__tests__/e2ee-object.test.ts +0 -90
- package/src/__tests__/e2ee-state.test.ts +0 -83
- package/src/__tests__/sdk.test.ts +0 -427
- package/src/__tests__/sessions.test.ts +0 -63
- package/src/activity.ts +0 -19
- package/src/auth.ts +0 -134
- package/src/connections.ts +0 -106
- package/src/data.ts +0 -88
- package/src/db.ts +0 -34
- package/src/directory.ts +0 -31
- package/src/e2ee-crypto.ts +0 -260
- package/src/e2ee-device.ts +0 -75
- package/src/e2ee-object.ts +0 -118
- package/src/e2ee-state.ts +0 -72
- package/src/e2ee.ts +0 -65
- package/src/env.ts +0 -37
- package/src/errors.ts +0 -46
- package/src/host.ts +0 -251
- package/src/index.ts +0 -160
- package/src/mailer.ts +0 -7
- package/src/notifications.ts +0 -36
- package/src/platform.ts +0 -10
- package/src/plugins.ts +0 -146
- package/src/portability.ts +0 -176
- package/src/secrets.ts +0 -68
- package/src/sessions.ts +0 -36
- package/src/storage.ts +0 -49
- package/src/types.ts +0 -418
- package/src/unimplemented.ts +0 -48
package/dist/index.js
CHANGED
|
@@ -1,652 +1,1363 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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/
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
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/
|
|
19
|
-
var
|
|
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.
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
const
|
|
95
|
-
return
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
await
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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/
|
|
255
|
-
|
|
256
|
-
var
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
//
|
|
311
|
-
var
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return
|
|
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
|
-
//
|
|
367
|
-
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
//
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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/
|
|
425
|
-
|
|
426
|
-
function
|
|
427
|
-
if (
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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/
|
|
464
|
-
|
|
465
|
-
var
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
async
|
|
478
|
-
const
|
|
479
|
-
if (!
|
|
480
|
-
throw new
|
|
481
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
|
505
|
-
|
|
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
|
-
|
|
511
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
647
|
-
|
|
1353
|
+
ConfigurationError,
|
|
1354
|
+
CoreApiError,
|
|
1355
|
+
CoreUnavailableError,
|
|
648
1356
|
NotAuthenticatedError,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1357
|
+
NotSupportedError,
|
|
1358
|
+
SessionEndedError,
|
|
1359
|
+
SignInError,
|
|
1360
|
+
SovereignError,
|
|
1361
|
+
createSovereign,
|
|
1362
|
+
hasRole
|
|
652
1363
|
};
|