@k9o/gate 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +270 -2
- package/dist/chunk-JEV37YEC.js +51 -0
- package/dist/chunk-JEV37YEC.js.map +1 -0
- package/dist/chunk-XP6U5QKF.js +41 -0
- package/dist/chunk-XP6U5QKF.js.map +1 -0
- package/dist/chunk-YINWOWJC.js +125 -0
- package/dist/chunk-YINWOWJC.js.map +1 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +715 -0
- package/dist/index.js +613 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +84 -0
- package/dist/jwt.js +12 -0
- package/dist/jwt.js.map +1 -0
- package/dist/mode.d.ts +82 -0
- package/dist/mode.js +13 -0
- package/dist/mode.js.map +1 -0
- package/dist/types.d.ts +250 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -4
- package/src/client-factory.ts +39 -0
- package/src/client.ts +175 -0
- package/src/errors.ts +115 -0
- package/src/gate.ts +384 -0
- package/src/idp-providers/identity-providers.ts +244 -0
- package/src/index.ts +62 -0
- package/src/jwt.ts +229 -0
- package/src/magic-link-redirects.ts +103 -0
- package/src/mode.ts +115 -0
- package/src/paths.ts +21 -0
- package/src/region.ts +85 -0
- package/src/types.ts +264 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractClaims,
|
|
3
|
+
requireScope,
|
|
4
|
+
verifyJwt
|
|
5
|
+
} from "./chunk-YINWOWJC.js";
|
|
6
|
+
import {
|
|
7
|
+
GateError,
|
|
8
|
+
gateErrorFromWire
|
|
9
|
+
} from "./chunk-JEV37YEC.js";
|
|
10
|
+
import {
|
|
11
|
+
configureGate,
|
|
12
|
+
getGateConfig,
|
|
13
|
+
resetGateConfig,
|
|
14
|
+
validateGateConfig
|
|
15
|
+
} from "./chunk-XP6U5QKF.js";
|
|
16
|
+
|
|
17
|
+
// src/region.ts
|
|
18
|
+
var REGION_FREE_ORG_HOST = /^([a-z][a-z0-9-]{1,62})\.(api|api-staging)\.k9o\.io$/;
|
|
19
|
+
var REGION_CODE = /^[a-z]{2}\d{2}$/;
|
|
20
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
function regionalOriginFromLocation(host, location, base) {
|
|
22
|
+
const m = REGION_FREE_ORG_HOST.exec(host);
|
|
23
|
+
if (m === null) return null;
|
|
24
|
+
let u;
|
|
25
|
+
try {
|
|
26
|
+
u = new URL(location, base);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (u.protocol !== "https:" || u.username !== "" || u.password !== "" || u.port !== "") return null;
|
|
31
|
+
const [, org, apex] = m;
|
|
32
|
+
const want = new RegExp(`^${org}\\.${apex}-([a-z0-9]+)\\.k9o\\.io$`);
|
|
33
|
+
const r = want.exec(u.hostname);
|
|
34
|
+
if (r === null || !REGION_CODE.test(r[1])) return null;
|
|
35
|
+
return `https://${u.hostname}`;
|
|
36
|
+
}
|
|
37
|
+
async function probe(config) {
|
|
38
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
39
|
+
const configured = config.baseUrl;
|
|
40
|
+
if (typeof fetchImpl !== "function") return { origin: configured, cacheable: false };
|
|
41
|
+
let host;
|
|
42
|
+
try {
|
|
43
|
+
host = new URL(configured).hostname;
|
|
44
|
+
} catch {
|
|
45
|
+
return { origin: configured, cacheable: true };
|
|
46
|
+
}
|
|
47
|
+
if (!REGION_FREE_ORG_HOST.test(host)) return { origin: configured, cacheable: true };
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetchImpl(`${configured.replace(/\/+$/, "")}/`, {
|
|
50
|
+
method: "HEAD",
|
|
51
|
+
redirect: "manual",
|
|
52
|
+
headers: { accept: "*/*" }
|
|
53
|
+
});
|
|
54
|
+
if (res.status === 308) {
|
|
55
|
+
const loc = res.headers.get("location");
|
|
56
|
+
const origin = loc === null ? null : regionalOriginFromLocation(host, loc, configured);
|
|
57
|
+
if (origin !== null) return { origin, cacheable: true };
|
|
58
|
+
}
|
|
59
|
+
return { origin: configured, cacheable: true };
|
|
60
|
+
} catch {
|
|
61
|
+
return { origin: configured, cacheable: false };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function resolveRegionalOrigin(config) {
|
|
65
|
+
const hit = cache.get(config);
|
|
66
|
+
if (hit !== void 0) return hit;
|
|
67
|
+
const p = probe(config).then((r) => {
|
|
68
|
+
if (!r.cacheable) cache.delete(config);
|
|
69
|
+
return r.origin;
|
|
70
|
+
});
|
|
71
|
+
cache.set(config, p);
|
|
72
|
+
return p;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/client.ts
|
|
76
|
+
async function requestWith(config, init) {
|
|
77
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
78
|
+
if (typeof fetchImpl !== "function") {
|
|
79
|
+
throw new GateError({
|
|
80
|
+
code: "INTERNAL_ERROR",
|
|
81
|
+
message: "No fetch implementation available \u2014 configure {fetch} in configureGate() for non-browser, non-Bun, non-Node-18+ environments",
|
|
82
|
+
context: {},
|
|
83
|
+
recommendation: null
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const url = buildUrl(await resolveRegionalOrigin(config), init.path, init.query);
|
|
87
|
+
const headers = {
|
|
88
|
+
accept: "application/json",
|
|
89
|
+
"x-gate-mode": config.mode,
|
|
90
|
+
...init.headers ?? {}
|
|
91
|
+
};
|
|
92
|
+
const bearer = init.bearerToken ?? config.partnerToken;
|
|
93
|
+
if (typeof bearer === "string" && bearer.length > 0) {
|
|
94
|
+
headers["authorization"] = `Bearer ${bearer}`;
|
|
95
|
+
}
|
|
96
|
+
const requestInit = {
|
|
97
|
+
method: init.method,
|
|
98
|
+
headers
|
|
99
|
+
};
|
|
100
|
+
if (init.body !== void 0) {
|
|
101
|
+
headers["content-type"] = "application/json";
|
|
102
|
+
requestInit["body"] = JSON.stringify(init.body);
|
|
103
|
+
}
|
|
104
|
+
let response;
|
|
105
|
+
try {
|
|
106
|
+
response = await fetchImpl(url, requestInit);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new GateError({
|
|
109
|
+
code: "UPSTREAM_UNAVAILABLE",
|
|
110
|
+
message: `Network error reaching ${url}: ${e instanceof Error ? e.message : String(e)}`,
|
|
111
|
+
context: { url },
|
|
112
|
+
recommendation: null
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = await response.json();
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw new GateError({
|
|
120
|
+
code: "INTERNAL_ERROR",
|
|
121
|
+
message: `Failed to parse JSON response from ${url}: ${e instanceof Error ? e.message : String(e)}`,
|
|
122
|
+
context: { url, status: response.status },
|
|
123
|
+
recommendation: null,
|
|
124
|
+
status: response.status
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (response.status >= 200 && response.status < 300) {
|
|
128
|
+
if (!isEnvelope(parsed)) {
|
|
129
|
+
throw new GateError({
|
|
130
|
+
code: "INTERNAL_ERROR",
|
|
131
|
+
message: `Malformed envelope from ${url}: expected {data, meta}`,
|
|
132
|
+
context: { url, status: response.status },
|
|
133
|
+
recommendation: null,
|
|
134
|
+
status: response.status
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
if (isKErrorWire(parsed)) {
|
|
140
|
+
throw gateErrorFromWire(parsed, response.status);
|
|
141
|
+
}
|
|
142
|
+
throw new GateError({
|
|
143
|
+
code: "INTERNAL_ERROR",
|
|
144
|
+
message: `Upstream returned ${response.status} with non-KError body`,
|
|
145
|
+
context: { url, status: response.status, body: parsed },
|
|
146
|
+
recommendation: null,
|
|
147
|
+
status: response.status
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function buildUrl(baseUrl, path, query) {
|
|
151
|
+
const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
152
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`;
|
|
153
|
+
let url = `${trimmedBase}${trimmedPath}`;
|
|
154
|
+
if (query !== void 0) {
|
|
155
|
+
const params = new URLSearchParams();
|
|
156
|
+
for (const [k, v] of Object.entries(query)) {
|
|
157
|
+
if (v !== void 0) params.set(k, String(v));
|
|
158
|
+
}
|
|
159
|
+
const qs = params.toString();
|
|
160
|
+
if (qs.length > 0) url += `?${qs}`;
|
|
161
|
+
}
|
|
162
|
+
return url;
|
|
163
|
+
}
|
|
164
|
+
function isEnvelope(value) {
|
|
165
|
+
if (value === null || typeof value !== "object") return false;
|
|
166
|
+
const v = value;
|
|
167
|
+
return "data" in v && "meta" in v && typeof v.meta === "object" && v.meta !== null;
|
|
168
|
+
}
|
|
169
|
+
function isKErrorWire(value) {
|
|
170
|
+
if (value === null || typeof value !== "object") return false;
|
|
171
|
+
const v = value;
|
|
172
|
+
return typeof v.code === "string" && typeof v.message === "string";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/paths.ts
|
|
176
|
+
function resolveProjectPathRoot(config) {
|
|
177
|
+
return `/@${config.projectSlug}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/idp-providers/identity-providers.ts
|
|
181
|
+
function identityProvidersPath(config) {
|
|
182
|
+
return `${resolveProjectPathRoot(config)}/identity-providers`;
|
|
183
|
+
}
|
|
184
|
+
function makeIdentityProviders(getConfig) {
|
|
185
|
+
return {
|
|
186
|
+
/**
|
|
187
|
+
* Register a new end-user IdP provider config for the configured
|
|
188
|
+
* project. PROJECT_ADMIN+ required (server-enforced). The plaintext
|
|
189
|
+
* client secret is encrypted-at-rest server-side; the returned config
|
|
190
|
+
* carries only the opaque secret handle.
|
|
191
|
+
*
|
|
192
|
+
* The gateway validates the `idpProvider`→`tierGating` pairing
|
|
193
|
+
* and rejects a mismatch with `K9O_INVALID_FIELD_VALUE`.
|
|
194
|
+
*/
|
|
195
|
+
async create(input) {
|
|
196
|
+
const config = getConfig();
|
|
197
|
+
return requestWith(config, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
path: identityProvidersPath(config),
|
|
200
|
+
body: input
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
/**
|
|
204
|
+
* List the configured end-user IdP provider configs for the project.
|
|
205
|
+
* READ_ROLES-gated server-side (PROJECT_ADMIN/EDITOR/MEMBER/VIEWER/AUTHOR/
|
|
206
|
+
* ANALYST) — the SAME shape + roles as the existing GET route
|
|
207
|
+
* (`listProviders` returns the existing route's shape, no
|
|
208
|
+
* confidentiality-narrower endpoint). Each `IdpConfig` carries the opaque
|
|
209
|
+
* secret handle, never the secret. Callers that only need the summary
|
|
210
|
+
* (`id`/`idpProvider`/`displayName`/`enabled`/`tierGating`) pick fields off
|
|
211
|
+
* the result — no server change needed for that shaping.
|
|
212
|
+
*/
|
|
213
|
+
async list() {
|
|
214
|
+
const config = getConfig();
|
|
215
|
+
return requestWith(config, {
|
|
216
|
+
method: "GET",
|
|
217
|
+
path: identityProvidersPath(config)
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* Test connectivity to a configured provider by id — a NO-SECRET reachability + metadata-validity probe
|
|
222
|
+
* (no-secret): the gateway resolve-and-pins the issuer/metadata endpoint (SSRF-fenced) and
|
|
223
|
+
* validates it structurally, WITHOUT decrypting or transmitting the stored client secret. Returns
|
|
224
|
+
* `{ok, reason?}`; the `reason` is a closed machine code, never the upstream body or any credential.
|
|
225
|
+
* PROJECT_ADMIN required (server-enforced) — it triggers live external egress on the platform's behalf.
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* Update a provider config by id (PATCH; PROJECT_ADMIN, server-enforced). Partial: only supplied fields
|
|
229
|
+
* change; `idpProvider`/`tierGating` are immutable. Returns the updated config (secrets never included).
|
|
230
|
+
* GS-F7: write directory-sync settings here and read their status back off `list()`. See `PatchIdpInput`
|
|
231
|
+
* for the metadataUrl-change-clears-secret rule.
|
|
232
|
+
*/
|
|
233
|
+
async update(configId, patch) {
|
|
234
|
+
const config = getConfig();
|
|
235
|
+
return requestWith(config, {
|
|
236
|
+
method: "PATCH",
|
|
237
|
+
path: `${identityProvidersPath(config)}/${encodeURIComponent(configId)}`,
|
|
238
|
+
body: patch
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
async testProvider(configId) {
|
|
242
|
+
const config = getConfig();
|
|
243
|
+
return requestWith(config, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
path: `${identityProvidersPath(config)}/${encodeURIComponent(configId)}/test`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
var identityProviders = makeIdentityProviders(getGateConfig);
|
|
251
|
+
|
|
252
|
+
// src/magic-link-redirects.ts
|
|
253
|
+
function magicLinkRedirectsPath(config, id) {
|
|
254
|
+
const root = `${resolveProjectPathRoot(config)}/gate/magic-link-redirects`;
|
|
255
|
+
return id !== void 0 ? `${root}/${id}` : root;
|
|
256
|
+
}
|
|
257
|
+
function makeMagicLinkRedirects(getConfig) {
|
|
258
|
+
return {
|
|
259
|
+
/** List every allowlisted redirect target for the active project. */
|
|
260
|
+
async list() {
|
|
261
|
+
const config = getConfig();
|
|
262
|
+
return requestWith(config, {
|
|
263
|
+
method: "GET",
|
|
264
|
+
path: magicLinkRedirectsPath(config)
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
/**
|
|
268
|
+
* Register a new redirect target. `redirectUrl` must exact-match
|
|
269
|
+
* what a customer later passes to `requestMagicLink` — the gateway
|
|
270
|
+
* enforces exact match, never a prefix or wildcard.
|
|
271
|
+
*/
|
|
272
|
+
async add(input) {
|
|
273
|
+
const config = getConfig();
|
|
274
|
+
return requestWith(config, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
path: magicLinkRedirectsPath(config),
|
|
277
|
+
body: input
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
/** Remove a previously-registered redirect target by its `id`. */
|
|
281
|
+
async remove(id) {
|
|
282
|
+
const config = getConfig();
|
|
283
|
+
return requestWith(config, {
|
|
284
|
+
method: "DELETE",
|
|
285
|
+
path: magicLinkRedirectsPath(config, id)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
var magicLinkRedirects = makeMagicLinkRedirects(getGateConfig);
|
|
291
|
+
|
|
292
|
+
// src/gate.ts
|
|
293
|
+
function gatePath(config, suffix) {
|
|
294
|
+
return `${resolveProjectPathRoot(config)}/gate/${suffix}`;
|
|
295
|
+
}
|
|
296
|
+
function makeGateNamespace(getConfig) {
|
|
297
|
+
return {
|
|
298
|
+
/**
|
|
299
|
+
* Create a new end-user identity for the configured project. Returns
|
|
300
|
+
* the new user record + an initial session. Emits a
|
|
301
|
+
* `data.write.auth.signup` audit event.
|
|
302
|
+
*
|
|
303
|
+
* Errors (closed taxonomy):
|
|
304
|
+
* - `EMAIL_ALREADY_REGISTERED` (409) when the email collides
|
|
305
|
+
* - `INVALID_FIELD_VALUE` (400) on schema violation (password policy, malformed email)
|
|
306
|
+
* - `TIER_FEATURE_GATED` (403) when the project's tier blocks email/password
|
|
307
|
+
*/
|
|
308
|
+
async signUp(input) {
|
|
309
|
+
return requestWith(getConfig(), {
|
|
310
|
+
method: "POST",
|
|
311
|
+
path: gatePath(getConfig(), "signup"),
|
|
312
|
+
body: input
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
/**
|
|
316
|
+
* Sign in an existing end-user with email + password. Returns the
|
|
317
|
+
* user record + a fresh session. Emits a `data.write.auth.signin`
|
|
318
|
+
* audit event.
|
|
319
|
+
*
|
|
320
|
+
* Errors:
|
|
321
|
+
* - `INVALID_CREDENTIALS` (401) on bad email/password (ambiguous on
|
|
322
|
+
* purpose — does not distinguish "user not found" vs "password
|
|
323
|
+
* wrong" to avoid enumeration)
|
|
324
|
+
*/
|
|
325
|
+
async signIn(input) {
|
|
326
|
+
return requestWith(getConfig(), {
|
|
327
|
+
method: "POST",
|
|
328
|
+
path: gatePath(getConfig(), "signin"),
|
|
329
|
+
body: input
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
/**
|
|
333
|
+
* Sign out — revoke the bound refresh token (or an explicit one, if
|
|
334
|
+
* supplied). Emits a `data.write.auth.signout` audit event.
|
|
335
|
+
*
|
|
336
|
+
* Errors:
|
|
337
|
+
* - `UNAUTHENTICATED` (401) when no session is present
|
|
338
|
+
* - `REFRESH_TOKEN_INVALID` (400) when the supplied refresh token
|
|
339
|
+
* doesn't match the bearer's session
|
|
340
|
+
*/
|
|
341
|
+
async signOut(input = {}) {
|
|
342
|
+
return requestWith(getConfig(), {
|
|
343
|
+
method: "POST",
|
|
344
|
+
path: gatePath(getConfig(), "signout"),
|
|
345
|
+
body: input
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
/**
|
|
349
|
+
* Return the caller's identity claims from the bearer JWT. Useful in
|
|
350
|
+
* Standalone mode where the SDK consumer wants to surface the user's
|
|
351
|
+
* roles / groups without parsing the JWT itself.
|
|
352
|
+
*
|
|
353
|
+
* Errors:
|
|
354
|
+
* - `UNAUTHENTICATED` (401) when no bearer is present
|
|
355
|
+
* - `SESSION_EXPIRED` (401) when the bearer's exp has passed
|
|
356
|
+
*/
|
|
357
|
+
async whoami() {
|
|
358
|
+
return requestWith(getConfig(), {
|
|
359
|
+
method: "GET",
|
|
360
|
+
path: gatePath(getConfig(), "whoami")
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* Rotate the access token using a refresh token. Returns a fresh
|
|
365
|
+
* session pair (new access token + same refresh token; gateway may
|
|
366
|
+
* also rotate the refresh token per project policy).
|
|
367
|
+
*
|
|
368
|
+
* Errors:
|
|
369
|
+
* - `REFRESH_TOKEN_INVALID` (400) when the refresh token is unknown,
|
|
370
|
+
* expired, or revoked
|
|
371
|
+
*/
|
|
372
|
+
async refreshToken(input) {
|
|
373
|
+
return requestWith(getConfig(), {
|
|
374
|
+
method: "POST",
|
|
375
|
+
path: gatePath(getConfig(), "refresh"),
|
|
376
|
+
body: input
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
/**
|
|
380
|
+
* Check whether the bearer's session is currently valid. Does NOT
|
|
381
|
+
* throw on expiry or invalid token — returns `{ valid: false }`
|
|
382
|
+
* inside the envelope. The intended use is health-checking a session
|
|
383
|
+
* without surfacing an error to the end-user (the SDK consumer
|
|
384
|
+
* typically wants to redirect to sign-in rather than surface a 401).
|
|
385
|
+
*/
|
|
386
|
+
async verifySession() {
|
|
387
|
+
return requestWith(getConfig(), {
|
|
388
|
+
method: "GET",
|
|
389
|
+
path: gatePath(getConfig(), "verify")
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
/**
|
|
393
|
+
* The project's hosted sign-in page URL and WebAuthn `rpId`. Unauthenticated and cacheable; no credential is sent,
|
|
394
|
+
* even when a partner token is configured. Take both values as given: an unknown org, a deleted project and a
|
|
395
|
+
* non-Gate project all answer the same `PROJECT_NOT_FOUND`, so a failure here says nothing about which.
|
|
396
|
+
*/
|
|
397
|
+
async hosted() {
|
|
398
|
+
return requestWith(getConfig(), {
|
|
399
|
+
method: "GET",
|
|
400
|
+
path: gatePath(getConfig(), "hosted"),
|
|
401
|
+
bearerToken: ""
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
/**
|
|
405
|
+
* Initiate an external-IdP sign-in flow (Google / GitHub / Microsoft).
|
|
406
|
+
* The SDK POSTs to the gateway's authorize endpoint, receives the
|
|
407
|
+
* provider's authorization URL with a signed-state JWT (carrying PKCE
|
|
408
|
+
* verifier + the supplied `redirectUri`), and redirects the browser.
|
|
409
|
+
*
|
|
410
|
+
* After the user authenticates with the IdP, the gateway processes the
|
|
411
|
+
* callback and redirects the browser back to `redirectUri` with session
|
|
412
|
+
* tokens in the URL fragment. Use `parseSignInCallback()` on the
|
|
413
|
+
* destination page to extract them.
|
|
414
|
+
*
|
|
415
|
+
* Example:
|
|
416
|
+
* await gate.signInWithProvider({
|
|
417
|
+
* provider: 'google',
|
|
418
|
+
* redirectUri: 'https://app.example.com/auth/callback',
|
|
419
|
+
* });
|
|
420
|
+
* // …user lands at /auth/callback#access_token=…&refresh_token=…
|
|
421
|
+
*
|
|
422
|
+
* Generic OIDC requires authorization-endpoint discovery and is not
|
|
423
|
+
* yet wired; use a hosted provider (google/github/microsoft) for now.
|
|
424
|
+
*/
|
|
425
|
+
async signInWithProvider(input) {
|
|
426
|
+
const env = await requestWith(getConfig(), {
|
|
427
|
+
method: "POST",
|
|
428
|
+
path: `${resolveProjectPathRoot(getConfig())}/gate/idp/${input.provider}/authorize`,
|
|
429
|
+
body: { redirectUri: input.redirectUri }
|
|
430
|
+
});
|
|
431
|
+
const w = globalThis.window;
|
|
432
|
+
if (w !== void 0) {
|
|
433
|
+
w.location.assign(env.data.authorizationUrl);
|
|
434
|
+
} else {
|
|
435
|
+
throw new Error(
|
|
436
|
+
"signInWithProvider requires a browser environment (window.location); for server-side initiation, fetch the authorize endpoint directly."
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
/**
|
|
441
|
+
* Parse session tokens out of the URL fragment delivered by an
|
|
442
|
+
* IdP callback redirect. Returns the session payload on success or an
|
|
443
|
+
* error object on failure. Pass `window.location.hash` (or any
|
|
444
|
+
* `#`-prefixed string) — leading `#` is stripped automatically.
|
|
445
|
+
*
|
|
446
|
+
* Side-effect-free: does not call the gateway and does not mutate
|
|
447
|
+
* any session storage. The consumer is responsible for persisting
|
|
448
|
+
* the returned tokens (e.g. via the app's auth context).
|
|
449
|
+
*/
|
|
450
|
+
parseSignInCallback(hash) {
|
|
451
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
452
|
+
const params = new URLSearchParams(raw);
|
|
453
|
+
const error = params.get("error");
|
|
454
|
+
if (error !== null) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
error,
|
|
458
|
+
errorDescription: params.get("error_description") ?? error
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const accessToken = params.get("access_token");
|
|
462
|
+
const refreshToken = params.get("refresh_token");
|
|
463
|
+
const expiresAt = params.get("expires_at");
|
|
464
|
+
if (accessToken === null || refreshToken === null || expiresAt === null) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
error: "malformed_callback",
|
|
468
|
+
errorDescription: "Callback URL fragment missing one or more required fields (access_token, refresh_token, expires_at)."
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
accessToken,
|
|
474
|
+
refreshToken,
|
|
475
|
+
expiresAt,
|
|
476
|
+
tokenType: params.get("token_type") ?? "Bearer"
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
/**
|
|
480
|
+
* Passwordless sub-namespace — OTP + Magic Link flows
|
|
481
|
+
* for email-based authentication without a stored password.
|
|
482
|
+
*
|
|
483
|
+
* Wire endpoints (LIVE on the Gate end-user plane — verified against
|
|
484
|
+
* staging): OTP → `POST /@<projectSlug>/gate/otp/{request,verify}`
|
|
485
|
+
* (routes/otp.ts), Magic Link → `POST /@<projectSlug>/gate/magic-link/
|
|
486
|
+
* {send,redeem}` (routes/magic-link.ts). NOTE: these are the gateway's
|
|
487
|
+
* canonical paths; the SDK ergonomic method names (`requestMagicLink`/
|
|
488
|
+
* `verifyMagicLink`) do not mirror the path segments (`send`/`redeem`).
|
|
489
|
+
* (The SDK paths match the shipped gateway routes.)
|
|
490
|
+
*/
|
|
491
|
+
passwordless: {
|
|
492
|
+
/**
|
|
493
|
+
* Request a one-time passcode delivered to the supplied email.
|
|
494
|
+
* The OTP is short-lived (typically 5 min) and
|
|
495
|
+
* single-use; redemption via `verifyOtp` mints a session.
|
|
496
|
+
*/
|
|
497
|
+
async requestOtp(input) {
|
|
498
|
+
return requestWith(getConfig(), {
|
|
499
|
+
method: "POST",
|
|
500
|
+
path: gatePath(getConfig(), "otp/request"),
|
|
501
|
+
body: input
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
/**
|
|
505
|
+
* Verify a one-time passcode previously sent via `requestOtp`.
|
|
506
|
+
* On success returns the same `AuthResult` shape as `signIn` —
|
|
507
|
+
* a fresh session bound to the end-user identity.
|
|
508
|
+
*
|
|
509
|
+
* Errors:
|
|
510
|
+
* - INVALID_CREDENTIALS (401) on wrong / expired / consumed OTP
|
|
511
|
+
* (ambiguous on purpose — enumeration defense)
|
|
512
|
+
*/
|
|
513
|
+
async verifyOtp(input) {
|
|
514
|
+
return requestWith(getConfig(), {
|
|
515
|
+
method: "POST",
|
|
516
|
+
path: gatePath(getConfig(), "otp/verify"),
|
|
517
|
+
body: input
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
/**
|
|
521
|
+
* Request a magic-link emailed to the supplied address. The link
|
|
522
|
+
* carries an opaque short-lived token (typically 15 min
|
|
523
|
+
*); clicking the link lands at the customer's chosen
|
|
524
|
+
* redirect URL with the token in the query string, which the
|
|
525
|
+
* customer's frontend then exchanges via `verifyMagicLink`.
|
|
526
|
+
*/
|
|
527
|
+
async requestMagicLink(input) {
|
|
528
|
+
return requestWith(getConfig(), {
|
|
529
|
+
method: "POST",
|
|
530
|
+
path: gatePath(getConfig(), "magic-link/send"),
|
|
531
|
+
body: input
|
|
532
|
+
});
|
|
533
|
+
},
|
|
534
|
+
/**
|
|
535
|
+
* Verify a magic-link token previously delivered via
|
|
536
|
+
* `requestMagicLink`. On success returns the same `AuthResult`
|
|
537
|
+
* shape as `signIn`.
|
|
538
|
+
*
|
|
539
|
+
* Errors:
|
|
540
|
+
* - INVALID_CREDENTIALS (401) on wrong / expired / consumed
|
|
541
|
+
* token (same enumeration-defense rationale as verifyOtp)
|
|
542
|
+
*/
|
|
543
|
+
async verifyMagicLink(input) {
|
|
544
|
+
return requestWith(getConfig(), {
|
|
545
|
+
method: "POST",
|
|
546
|
+
path: gatePath(getConfig(), "magic-link/redeem"),
|
|
547
|
+
body: input
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
/**
|
|
551
|
+
* Extract the opaque magic-link token from the landing-page URL's query
|
|
552
|
+
* string (the gateway redirects to `<configured-redirect>?token=…&project=…`).
|
|
553
|
+
* Pure + side-effect-free — pass `window.location.search`. Feed the
|
|
554
|
+
* returned `token` to `verifyMagicLink({ token })` to mint the session.
|
|
555
|
+
*
|
|
556
|
+
* This is the magic-link counterpart to `parseSignInCallback` (which parses
|
|
557
|
+
* the SSO *hash fragment* for session tokens) — magic-link delivers an
|
|
558
|
+
* opaque token in the *query string*, so the two are not interchangeable.
|
|
559
|
+
*/
|
|
560
|
+
parseMagicLinkCallback(search) {
|
|
561
|
+
const raw = search.startsWith("?") ? search.slice(1) : search;
|
|
562
|
+
const params = new URLSearchParams(raw);
|
|
563
|
+
const error = params.get("error");
|
|
564
|
+
if (error !== null) {
|
|
565
|
+
return { ok: false, error };
|
|
566
|
+
}
|
|
567
|
+
const token = params.get("token");
|
|
568
|
+
if (token === null || token.length === 0) {
|
|
569
|
+
return { ok: false, error: "missing_token" };
|
|
570
|
+
}
|
|
571
|
+
return { ok: true, token };
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
/**
|
|
575
|
+
* Project-scoped end-user IdP provider-config management.
|
|
576
|
+
* PROJECT_ADMIN+ required; end-user JWTs rejected at the route. Only
|
|
577
|
+
* `create()` is bound — the REST create the Gate Studio
|
|
578
|
+
* `FederationPresetsPanel` routes to when it drops the `gate_flows`
|
|
579
|
+
* `__addFromPreset` sentinel. Gate-admin surface.
|
|
580
|
+
*/
|
|
581
|
+
identityProviders: makeIdentityProviders(getConfig),
|
|
582
|
+
/**
|
|
583
|
+
* Per-project magic-link redirect-URL allowlist management (blessed
|
|
584
|
+
* shape). Gate-admin surface alongside `identityProviders`
|
|
585
|
+
* — PROJECT_ADMIN+ required, distinct from the end-user-facing
|
|
586
|
+
* `passwordless.requestMagicLink`/`verifyMagicLink` send/verify flow.
|
|
587
|
+
*/
|
|
588
|
+
magicLinkRedirects: makeMagicLinkRedirects(getConfig)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
var gate = makeGateNamespace(getGateConfig);
|
|
592
|
+
|
|
593
|
+
// src/client-factory.ts
|
|
594
|
+
function createGateClient(config) {
|
|
595
|
+
validateGateConfig(config);
|
|
596
|
+
const frozen = { ...config };
|
|
597
|
+
return makeGateNamespace(() => frozen);
|
|
598
|
+
}
|
|
599
|
+
export {
|
|
600
|
+
GateError,
|
|
601
|
+
configureGate,
|
|
602
|
+
createGateClient,
|
|
603
|
+
extractClaims,
|
|
604
|
+
gate,
|
|
605
|
+
gateErrorFromWire,
|
|
606
|
+
getGateConfig,
|
|
607
|
+
regionalOriginFromLocation,
|
|
608
|
+
requireScope,
|
|
609
|
+
resetGateConfig,
|
|
610
|
+
resolveRegionalOrigin,
|
|
611
|
+
verifyJwt
|
|
612
|
+
};
|
|
613
|
+
//# sourceMappingURL=index.js.map
|