@k9o/gate 1.1.1 → 1.3.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 +28 -0
- package/README.md +58 -19
- package/dist/{chunk-JEV37YEC.js → chunk-7EED6YWR.js} +1 -1
- package/dist/chunk-7EED6YWR.js.map +1 -0
- package/dist/{chunk-YINWOWJC.js → chunk-OJQYLAGZ.js} +42 -5
- package/dist/chunk-OJQYLAGZ.js.map +1 -0
- package/dist/{chunk-XP6U5QKF.js → chunk-RPE5N37W.js} +1 -1
- package/dist/chunk-RPE5N37W.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +149 -239
- package/dist/index.js +75 -113
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +25 -36
- package/dist/jwt.js +4 -2
- package/dist/mode.d.ts +6 -33
- package/dist/mode.js +1 -1
- package/dist/types.d.ts +63 -99
- package/package.json +1 -1
- package/src/api-host.ts +43 -0
- package/src/client-factory.ts +2 -5
- package/src/client.ts +3 -3
- package/src/errors.ts +1 -1
- package/src/gate.ts +62 -134
- package/src/idp-providers/identity-providers.ts +8 -11
- package/src/index.ts +10 -6
- package/src/jwt.ts +63 -45
- package/src/magic-link-redirects.ts +1 -2
- package/src/mode.ts +6 -33
- package/src/paths.ts +2 -6
- package/src/region.ts +24 -24
- package/src/types.ts +60 -94
- package/dist/chunk-JEV37YEC.js.map +0 -1
- package/dist/chunk-XP6U5QKF.js.map +0 -1
- package/dist/chunk-YINWOWJC.js.map +0 -1
package/src/gate.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `gate.*` namespace, bound to the end-user Gate routes (project-scoped, `<org>.api.<apex>/@<projectSlug>/gate/...`):
|
|
3
|
+
* - `gate.signUp` POST /gate/signup
|
|
4
|
+
* - `gate.signIn` POST /gate/signin
|
|
5
|
+
* - `gate.signOut` POST /gate/signout
|
|
6
|
+
* - `gate.whoami` GET /gate/whoami
|
|
7
|
+
* - `gate.refreshToken` POST /gate/refresh
|
|
8
|
+
* - `gate.signInWithProvider` POST /gate/idp/{provider}/authorize
|
|
9
|
+
* - `gate.passwordless.*` POST /gate/otp/{request,verify} and /gate/magic-link/{send,redeem}
|
|
3
10
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - `gate.signIn` — POST /@<projectSlug>/gate/signin
|
|
7
|
-
* - `gate.signOut` — POST /@<projectSlug>/gate/signout
|
|
8
|
-
* - `gate.whoami` — GET /@<projectSlug>/gate/whoami
|
|
9
|
-
* - `gate.refreshToken` — POST /@<projectSlug>/gate/refresh
|
|
10
|
-
* - `gate.verifySession` — GET /@<projectSlug>/gate/verify
|
|
11
|
-
* - `gate.hosted` — GET /@<projectSlug>/gate/hosted (unauthenticated; `{ signInUrl, rpId }`)
|
|
12
|
-
*
|
|
13
|
-
* Sub-namespaces (passwordless / passkeys / social / mfa / password /
|
|
14
|
-
* users / email / sms / federation / admin) deferred to subsequent
|
|
15
|
-
* cycles per the README catalogue.
|
|
16
|
-
*
|
|
17
|
-
* Wire shape: every function returns the standard envelope
|
|
18
|
-
* `{ data, meta }` and throws a `GateError` on non-2xx
|
|
19
|
-
* gateway responses. Native vs Standalone mode does not affect SDK
|
|
20
|
-
* ergonomics; the mode header is set internally by the
|
|
21
|
-
* client + does not surface in the function-call ergonomics.
|
|
22
|
-
*
|
|
23
|
-
* Endpoint paths are reconciled to the LIVE gateway routes (verified
|
|
24
|
-
* against staging): signup/signin/signout/whoami/refresh/verify under
|
|
25
|
-
* `/@<projectSlug>/gate/*`, OTP under `/gate/otp/{request,verify}`,
|
|
26
|
-
* Magic Link under `/gate/magic-link/{send,redeem}`, IdP under
|
|
27
|
-
* `/gate/idp/:provider/{authorize,callback}`.
|
|
11
|
+
* Every function returns the standard envelope `{ data, meta }` and throws a `GateError` on a non-2xx gateway answer.
|
|
12
|
+
* The mode (native or standalone) does not change the call shapes; the client sends it as the `X-Gate-Mode` header.
|
|
28
13
|
*/
|
|
29
14
|
|
|
30
15
|
import { getGateConfig, type GateConfig } from "./mode.ts";
|
|
@@ -35,18 +20,22 @@ import { makeMagicLinkRedirects } from "./magic-link-redirects.ts";
|
|
|
35
20
|
import type {
|
|
36
21
|
AuthResult,
|
|
37
22
|
Envelope,
|
|
38
|
-
HostedGateInfo,
|
|
39
23
|
MagicLinkCallbackResult,
|
|
24
|
+
RefreshResult,
|
|
40
25
|
RefreshTokenInput,
|
|
26
|
+
RequestMagicLinkResult,
|
|
27
|
+
RequestOtpResult,
|
|
41
28
|
RequestMagicLinkInput,
|
|
42
29
|
RequestOtpInput,
|
|
43
30
|
SignInCallbackResult,
|
|
44
31
|
SignInInput,
|
|
45
32
|
SignOutInput,
|
|
33
|
+
SignOutResult,
|
|
46
34
|
SignUpInput,
|
|
47
35
|
VerifyMagicLinkInput,
|
|
36
|
+
VerifyMagicLinkResult,
|
|
48
37
|
VerifyOtpInput,
|
|
49
|
-
|
|
38
|
+
VerifyOtpResult,
|
|
50
39
|
WhoamiResult,
|
|
51
40
|
} from "./types.ts";
|
|
52
41
|
|
|
@@ -60,13 +49,12 @@ function gatePath(config: GateConfig, suffix: string): string {
|
|
|
60
49
|
export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
61
50
|
return {
|
|
62
51
|
/**
|
|
63
|
-
* Create a new end-user
|
|
64
|
-
*
|
|
65
|
-
* `data.write.auth.signup` audit event.
|
|
52
|
+
* Create a new end-user for the configured project. Returns the new user and an initial session. The body is
|
|
53
|
+
* closed: `{ email, password, displayName? }`.
|
|
66
54
|
*
|
|
67
|
-
* Errors
|
|
55
|
+
* Errors:
|
|
68
56
|
* - `EMAIL_ALREADY_REGISTERED` (409) when the email collides
|
|
69
|
-
* - `INVALID_FIELD_VALUE` (400) on schema violation (password policy, malformed email)
|
|
57
|
+
* - `INVALID_FIELD_VALUE` (400) on a schema violation (password policy, malformed email, unknown field)
|
|
70
58
|
* - `TIER_FEATURE_GATED` (403) when the project's tier blocks email/password
|
|
71
59
|
*/
|
|
72
60
|
async signUp(input: SignUpInput): Promise<Envelope<AuthResult>> {
|
|
@@ -78,14 +66,10 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
78
66
|
},
|
|
79
67
|
|
|
80
68
|
/**
|
|
81
|
-
* Sign in an existing end-user with email
|
|
82
|
-
* user record + a fresh session. Emits a `data.write.auth.signin`
|
|
83
|
-
* audit event.
|
|
69
|
+
* Sign in an existing end-user with email and password. Returns the user and a fresh session.
|
|
84
70
|
*
|
|
85
71
|
* Errors:
|
|
86
|
-
* - `INVALID_CREDENTIALS` (401) on bad email
|
|
87
|
-
* purpose — does not distinguish "user not found" vs "password
|
|
88
|
-
* wrong" to avoid enumeration)
|
|
72
|
+
* - `INVALID_CREDENTIALS` (401) on a bad email or password (the same answer for both, on purpose)
|
|
89
73
|
*/
|
|
90
74
|
async signIn(input: SignInInput): Promise<Envelope<AuthResult>> {
|
|
91
75
|
return requestWith<AuthResult>(getConfig(), {
|
|
@@ -96,16 +80,11 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
96
80
|
},
|
|
97
81
|
|
|
98
82
|
/**
|
|
99
|
-
* Sign out
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Errors:
|
|
103
|
-
* - `UNAUTHENTICATED` (401) when no session is present
|
|
104
|
-
* - `REFRESH_TOKEN_INVALID` (400) when the supplied refresh token
|
|
105
|
-
* doesn't match the bearer's session
|
|
83
|
+
* Sign out: revoke the session that the given refresh token belongs to. The route needs the refresh token in the
|
|
84
|
+
* body, so it is required here. Revoking an unknown or already-revoked token also answers `{ revoked: false }`.
|
|
106
85
|
*/
|
|
107
|
-
async signOut(input: SignOutInput
|
|
108
|
-
return requestWith<
|
|
86
|
+
async signOut(input: SignOutInput): Promise<Envelope<SignOutResult>> {
|
|
87
|
+
return requestWith<SignOutResult>(getConfig(), {
|
|
109
88
|
method: "POST",
|
|
110
89
|
path: gatePath(getConfig(), "signout"),
|
|
111
90
|
body: input,
|
|
@@ -113,13 +92,11 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
113
92
|
},
|
|
114
93
|
|
|
115
94
|
/**
|
|
116
|
-
* Return the
|
|
117
|
-
* Standalone mode where the SDK consumer wants to surface the user's
|
|
118
|
-
* roles / groups without parsing the JWT itself.
|
|
95
|
+
* Return the signed-in end-user (the bearer token's subject). The answer is the user record itself, not wrapped.
|
|
119
96
|
*
|
|
120
97
|
* Errors:
|
|
121
98
|
* - `UNAUTHENTICATED` (401) when no bearer is present
|
|
122
|
-
* - `SESSION_EXPIRED` (401) when the bearer
|
|
99
|
+
* - `SESSION_EXPIRED` (401) when the bearer has expired or was revoked
|
|
123
100
|
*/
|
|
124
101
|
async whoami(): Promise<Envelope<WhoamiResult>> {
|
|
125
102
|
return requestWith<WhoamiResult>(getConfig(), {
|
|
@@ -129,49 +106,20 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
129
106
|
},
|
|
130
107
|
|
|
131
108
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* also rotate the refresh token per project policy).
|
|
109
|
+
* Exchange a refresh token for a new access token. The answer carries only the new `accessToken` and its
|
|
110
|
+
* `expiresAt`; the refresh token you hold stays valid and is not returned again.
|
|
135
111
|
*
|
|
136
112
|
* Errors:
|
|
137
|
-
* - `
|
|
138
|
-
* expired, or revoked
|
|
113
|
+
* - `INVALID_CREDENTIALS` (401) when the refresh token is unknown, used or revoked
|
|
139
114
|
*/
|
|
140
|
-
async refreshToken(input: RefreshTokenInput): Promise<Envelope<
|
|
141
|
-
return requestWith<
|
|
115
|
+
async refreshToken(input: RefreshTokenInput): Promise<Envelope<RefreshResult>> {
|
|
116
|
+
return requestWith<RefreshResult>(getConfig(), {
|
|
142
117
|
method: "POST",
|
|
143
118
|
path: gatePath(getConfig(), "refresh"),
|
|
144
119
|
body: input,
|
|
145
120
|
});
|
|
146
121
|
},
|
|
147
122
|
|
|
148
|
-
/**
|
|
149
|
-
* Check whether the bearer's session is currently valid. Does NOT
|
|
150
|
-
* throw on expiry or invalid token — returns `{ valid: false }`
|
|
151
|
-
* inside the envelope. The intended use is health-checking a session
|
|
152
|
-
* without surfacing an error to the end-user (the SDK consumer
|
|
153
|
-
* typically wants to redirect to sign-in rather than surface a 401).
|
|
154
|
-
*/
|
|
155
|
-
async verifySession(): Promise<Envelope<VerifySessionResult>> {
|
|
156
|
-
return requestWith<VerifySessionResult>(getConfig(), {
|
|
157
|
-
method: "GET",
|
|
158
|
-
path: gatePath(getConfig(), "verify"),
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The project's hosted sign-in page URL and WebAuthn `rpId`. Unauthenticated and cacheable; no credential is sent,
|
|
164
|
-
* even when a partner token is configured. Take both values as given: an unknown org, a deleted project and a
|
|
165
|
-
* non-Gate project all answer the same `PROJECT_NOT_FOUND`, so a failure here says nothing about which.
|
|
166
|
-
*/
|
|
167
|
-
async hosted(): Promise<Envelope<HostedGateInfo>> {
|
|
168
|
-
return requestWith<HostedGateInfo>(getConfig(), {
|
|
169
|
-
method: "GET",
|
|
170
|
-
path: gatePath(getConfig(), "hosted"),
|
|
171
|
-
bearerToken: "",
|
|
172
|
-
});
|
|
173
|
-
},
|
|
174
|
-
|
|
175
123
|
/**
|
|
176
124
|
* Initiate an external-IdP sign-in flow (Google / GitHub / Microsoft).
|
|
177
125
|
* The SDK POSTs to the gateway's authorize endpoint, receives the
|
|
@@ -190,8 +138,6 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
190
138
|
* });
|
|
191
139
|
* // …user lands at /auth/callback#access_token=…&refresh_token=…
|
|
192
140
|
*
|
|
193
|
-
* Generic OIDC requires authorization-endpoint discovery and is not
|
|
194
|
-
* yet wired; use a hosted provider (google/github/microsoft) for now.
|
|
195
141
|
*/
|
|
196
142
|
async signInWithProvider(input: {
|
|
197
143
|
provider: "google" | "github" | "microsoft" | "oidc" | "saml";
|
|
@@ -254,25 +200,19 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
254
200
|
},
|
|
255
201
|
|
|
256
202
|
/**
|
|
257
|
-
* Passwordless
|
|
258
|
-
* for email-based authentication without a stored password.
|
|
203
|
+
* Passwordless sign-in: one-time passcode and magic link, both by email.
|
|
259
204
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* {send,redeem}` (routes/magic-link.ts). NOTE: these are the gateway's
|
|
264
|
-
* canonical paths; the SDK ergonomic method names (`requestMagicLink`/
|
|
265
|
-
* `verifyMagicLink`) do not mirror the path segments (`send`/`redeem`).
|
|
266
|
-
* (The SDK paths match the shipped gateway routes.)
|
|
205
|
+
* Routes: `POST /@<projectSlug>/gate/otp/{request,verify}` and `POST /@<projectSlug>/gate/magic-link/{send,redeem}`.
|
|
206
|
+
* The method names do not mirror the last path segment (`requestMagicLink` calls `send`, `verifyMagicLink` calls
|
|
207
|
+
* `redeem`).
|
|
267
208
|
*/
|
|
268
209
|
passwordless: {
|
|
269
210
|
/**
|
|
270
|
-
* Request a one-time passcode
|
|
271
|
-
* The
|
|
272
|
-
* single-use; redemption via `verifyOtp` mints a session.
|
|
211
|
+
* Request a one-time passcode for the email. It is short-lived and single-use; redeem it with `verifyOtp`.
|
|
212
|
+
* The answer carries the code's id and expiry (`data.otp`); the code itself goes by email.
|
|
273
213
|
*/
|
|
274
|
-
async requestOtp(input: RequestOtpInput): Promise<Envelope<
|
|
275
|
-
return requestWith<
|
|
214
|
+
async requestOtp(input: RequestOtpInput): Promise<Envelope<RequestOtpResult>> {
|
|
215
|
+
return requestWith<RequestOtpResult>(getConfig(), {
|
|
276
216
|
method: "POST",
|
|
277
217
|
path: gatePath(getConfig(), "otp/request"),
|
|
278
218
|
body: input,
|
|
@@ -280,16 +220,15 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
280
220
|
},
|
|
281
221
|
|
|
282
222
|
/**
|
|
283
|
-
* Verify a one-time passcode previously sent via `requestOtp`.
|
|
284
|
-
*
|
|
285
|
-
*
|
|
223
|
+
* Verify a one-time passcode previously sent via `requestOtp`. For the default `signin` purpose the answer
|
|
224
|
+
* carries a fresh `session`; for any other purpose it only confirms (`verified: true`) and the caller continues
|
|
225
|
+
* its own flow.
|
|
286
226
|
*
|
|
287
227
|
* Errors:
|
|
288
|
-
* - INVALID_CREDENTIALS (401) on wrong
|
|
289
|
-
* (ambiguous on purpose — enumeration defense)
|
|
228
|
+
* - `INVALID_CREDENTIALS` (401) on a wrong, expired or used code
|
|
290
229
|
*/
|
|
291
|
-
async verifyOtp(input: VerifyOtpInput): Promise<Envelope<
|
|
292
|
-
return requestWith<
|
|
230
|
+
async verifyOtp(input: VerifyOtpInput): Promise<Envelope<VerifyOtpResult>> {
|
|
231
|
+
return requestWith<VerifyOtpResult>(getConfig(), {
|
|
293
232
|
method: "POST",
|
|
294
233
|
path: gatePath(getConfig(), "otp/verify"),
|
|
295
234
|
body: input,
|
|
@@ -297,16 +236,12 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
297
236
|
},
|
|
298
237
|
|
|
299
238
|
/**
|
|
300
|
-
* Request a magic
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* redirect URL with the token in the query string, which the
|
|
304
|
-
* customer's frontend then exchanges via `verifyMagicLink`.
|
|
239
|
+
* Request a magic link by email. The link carries an opaque short-lived token and lands at the project's
|
|
240
|
+
* redirect URL with the token in the query string; exchange it with `verifyMagicLink`. The answer is
|
|
241
|
+
* `data.magicLink` with `sent` and the expiry.
|
|
305
242
|
*/
|
|
306
|
-
async requestMagicLink(
|
|
307
|
-
|
|
308
|
-
): Promise<Envelope<{ requested: boolean }>> {
|
|
309
|
-
return requestWith<{ requested: boolean }>(getConfig(), {
|
|
243
|
+
async requestMagicLink(input: RequestMagicLinkInput): Promise<Envelope<RequestMagicLinkResult>> {
|
|
244
|
+
return requestWith<RequestMagicLinkResult>(getConfig(), {
|
|
310
245
|
method: "POST",
|
|
311
246
|
path: gatePath(getConfig(), "magic-link/send"),
|
|
312
247
|
body: input,
|
|
@@ -314,23 +249,21 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
314
249
|
},
|
|
315
250
|
|
|
316
251
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
* shape as `signIn`.
|
|
252
|
+
* Redeem a magic-link token previously delivered via `requestMagicLink`. The answer carries the new `session`
|
|
253
|
+
* and the `endUserId` (not the full user record; call `whoami` for that).
|
|
320
254
|
*
|
|
321
255
|
* Errors:
|
|
322
|
-
* - INVALID_CREDENTIALS (401) on wrong
|
|
323
|
-
* token (same enumeration-defense rationale as verifyOtp)
|
|
256
|
+
* - `INVALID_CREDENTIALS` (401) on a wrong, expired or used token
|
|
324
257
|
*/
|
|
325
|
-
async verifyMagicLink(input: VerifyMagicLinkInput): Promise<Envelope<
|
|
326
|
-
return requestWith<
|
|
258
|
+
async verifyMagicLink(input: VerifyMagicLinkInput): Promise<Envelope<VerifyMagicLinkResult>> {
|
|
259
|
+
return requestWith<VerifyMagicLinkResult>(getConfig(), {
|
|
327
260
|
method: "POST",
|
|
328
261
|
path: gatePath(getConfig(), "magic-link/redeem"),
|
|
329
262
|
body: input,
|
|
330
263
|
});
|
|
331
264
|
},
|
|
332
265
|
|
|
333
|
-
|
|
266
|
+
/**
|
|
334
267
|
* Extract the opaque magic-link token from the landing-page URL's query
|
|
335
268
|
* string (the gateway redirects to `<configured-redirect>?token=…&project=…`).
|
|
336
269
|
* Pure + side-effect-free — pass `window.location.search`. Feed the
|
|
@@ -356,19 +289,14 @@ export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
|
356
289
|
},
|
|
357
290
|
|
|
358
291
|
/**
|
|
359
|
-
* Project-scoped end-user IdP provider-config management.
|
|
360
|
-
* PROJECT_ADMIN+ required; end-user JWTs rejected at the route.
|
|
361
|
-
* `create()` is bound — the REST create the Gate Studio
|
|
362
|
-
* `FederationPresetsPanel` routes to when it drops the `gate_flows`
|
|
363
|
-
* `__addFromPreset` sentinel. Gate-admin surface.
|
|
292
|
+
* Project-scoped end-user IdP provider-config management (create, list, update, test).
|
|
293
|
+
* PROJECT_ADMIN+ required; end-user JWTs are rejected at the route.
|
|
364
294
|
*/
|
|
365
295
|
identityProviders: makeIdentityProviders(getConfig),
|
|
366
296
|
|
|
367
297
|
/**
|
|
368
|
-
* Per-project magic-link redirect-URL allowlist management (
|
|
369
|
-
*
|
|
370
|
-
* — PROJECT_ADMIN+ required, distinct from the end-user-facing
|
|
371
|
-
* `passwordless.requestMagicLink`/`verifyMagicLink` send/verify flow.
|
|
298
|
+
* Per-project magic-link redirect-URL allowlist management (list, add, remove).
|
|
299
|
+
* PROJECT_ADMIN+ required; distinct from the end-user `passwordless` flow.
|
|
372
300
|
*/
|
|
373
301
|
magicLinkRedirects: makeMagicLinkRedirects(getConfig),
|
|
374
302
|
};
|
|
@@ -22,12 +22,10 @@
|
|
|
22
22
|
* `partnerToken` in config, or per-call via the client's bearer plumbing.
|
|
23
23
|
*
|
|
24
24
|
* 🔴 `clientSecretRef` is a RAW SECRET on the way in, not a reference.
|
|
25
|
-
*
|
|
25
|
+
* The gateway encrypts the submitted value (AES-256-GCM under
|
|
26
26
|
* `K9O_SECRET_STORAGE_MASTER_KEY`) into the opaque `client_secret_enc`
|
|
27
27
|
* handle and persists only that; the field name is retained for wire
|
|
28
|
-
* compatibility
|
|
29
|
-
* (`project-identity-providers.ts`, the v105 INSERT) is authoritative —
|
|
30
|
-
* there is NO Kibo custody on this path. Reads NEVER carry the secret or its
|
|
28
|
+
* compatibility and there is no Kibo custody on this path. Reads NEVER carry the secret or its
|
|
31
29
|
* handle: they carry only `clientSecretSet` (a presence boolean,
|
|
32
30
|
* the write-only fix). `serviceAccountKey` (directory sync) follows the same write-only rule.
|
|
33
31
|
*/
|
|
@@ -37,8 +35,7 @@ import { requestWith } from "../client.ts";
|
|
|
37
35
|
import { resolveProjectPathRoot } from "../paths.ts";
|
|
38
36
|
import type { Envelope } from "../types.ts";
|
|
39
37
|
|
|
40
|
-
/** Closed end-user-class provider enum
|
|
41
|
-
* `_sys.end_user_idp_provider_configs`); the org plane carries the same 11 since v222/v223. */
|
|
38
|
+
/** Closed end-user-class provider enum (11 values). */
|
|
42
39
|
export type IdpProviderKind =
|
|
43
40
|
| "google" | "github" | "microsoft" | "oidc" | "saml" | "scim"
|
|
44
41
|
| "ldap" | "active_directory" | "apple" | "facebook" | "twitter";
|
|
@@ -60,7 +57,7 @@ export type FederationTestProviderResult = {
|
|
|
60
57
|
| "kind_unsupported";
|
|
61
58
|
};
|
|
62
59
|
|
|
63
|
-
/**
|
|
60
|
+
/** The provider→tier pairing is structurally fixed:
|
|
64
61
|
* `free_tier_ok ∈ {google, github, microsoft, apple, facebook, twitter}`,
|
|
65
62
|
* `ridge_plus ∈ {oidc, saml, scim, ldap, active_directory}`. The gateway rejects a mismatched
|
|
66
63
|
* pairing (`K9O_INVALID_FIELD_VALUE`); the SDK forwards the caller value
|
|
@@ -74,7 +71,7 @@ export type IdpTierGating = "free_tier_ok" | "ridge_plus";
|
|
|
74
71
|
*
|
|
75
72
|
* Reads carry `clientSecretSet` (presence only), never the secret or a handle (see the module header).
|
|
76
73
|
* `metadataUrl`/`updatedAt` are `null` until set; `attributeMap` defaults to `{}`; `allowedRedirectUris`
|
|
77
|
-
* defaults to `[]`. The `directory*` fields are
|
|
74
|
+
* defaults to `[]`. The `directory*` fields are scheduled directory sync (Google only):
|
|
78
75
|
* settings plus the scheduler's last-run status; the service-account key itself is never returned.
|
|
79
76
|
*/
|
|
80
77
|
export type IdpConfig = {
|
|
@@ -100,7 +97,7 @@ export type IdpConfig = {
|
|
|
100
97
|
updatedAt: string | null;
|
|
101
98
|
};
|
|
102
99
|
|
|
103
|
-
/**
|
|
100
|
+
/** Directory-sync write fields — accepted on create AND update (one PROJECT_ADMIN surface). */
|
|
104
101
|
export type DirectorySyncInput = {
|
|
105
102
|
/** Enabling requires `idpProvider: "google"`, a `serviceAccountKey` (now or already stored) and an interval. */
|
|
106
103
|
directoryPullEnabled?: boolean;
|
|
@@ -147,7 +144,7 @@ export type CreateIdpInput = {
|
|
|
147
144
|
* `tierGating` are immutable after create. An empty patch returns the current config unchanged.
|
|
148
145
|
*
|
|
149
146
|
* 🔴 For `oidc`/`microsoft`, changing `metadataUrl` WITHOUT `clientSecretRef` in the same patch CLEARS the
|
|
150
|
-
* stored client secret (the gateway's secret-invalidate-on-host-change control
|
|
147
|
+
* stored client secret (the gateway's secret-invalidate-on-host-change control): re-supply the
|
|
151
148
|
* secret whenever the metadata URL changes.
|
|
152
149
|
*/
|
|
153
150
|
export type PatchIdpInput = {
|
|
@@ -216,7 +213,7 @@ export function makeIdentityProviders(getConfig: () => GateConfig) {
|
|
|
216
213
|
/**
|
|
217
214
|
* Update a provider config by id (PATCH; PROJECT_ADMIN, server-enforced). Partial: only supplied fields
|
|
218
215
|
* change; `idpProvider`/`tierGating` are immutable. Returns the updated config (secrets never included).
|
|
219
|
-
*
|
|
216
|
+
* Write directory-sync settings here and read their status back off `list()`. See `PatchIdpInput`
|
|
220
217
|
* for the metadataUrl-change-clears-secret rule.
|
|
221
218
|
*/
|
|
222
219
|
async update(configId: string, patch: PatchIdpInput): Promise<Envelope<IdpConfig>> {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@k9o/gate` — Gate (Identity-Aware Kernel) SDK entry point.
|
|
3
3
|
*
|
|
4
|
-
* Re-exports the
|
|
4
|
+
* Re-exports the public surface:
|
|
5
5
|
* - `gate` — marquee namespace + sub-namespaces
|
|
6
6
|
* - `configureGate` / `getGateConfig` / `resetGateConfig` — mode + config plumbing
|
|
7
7
|
* - `verifyJwt` / `extractClaims` / `requireScope` — standalone JWT primitives
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
export { gate, type GateNamespace } from "./gate.ts";
|
|
13
13
|
export { createGateClient, type GateClient } from "./client-factory.ts";
|
|
14
|
+
export { parseApiHost, type ApiHost } from "./api-host.ts";
|
|
14
15
|
export { resolveRegionalOrigin, regionalOriginFromLocation, type RegionConfig } from "./region.ts";
|
|
15
16
|
export {
|
|
16
17
|
configureGate,
|
|
@@ -19,7 +20,7 @@ export {
|
|
|
19
20
|
type GateConfig,
|
|
20
21
|
type GateMode,
|
|
21
22
|
} from "./mode.ts";
|
|
22
|
-
export { verifyJwt, extractClaims, requireScope, type VerifyJwtOptions } from "./jwt.ts";
|
|
23
|
+
export { verifyJwt, fetchJwks, extractClaims, requireScope, type VerifyJwtOptions } from "./jwt.ts";
|
|
23
24
|
export {
|
|
24
25
|
GateError,
|
|
25
26
|
type GateErrorCode,
|
|
@@ -32,16 +33,20 @@ export type {
|
|
|
32
33
|
Session,
|
|
33
34
|
AuthResult,
|
|
34
35
|
WhoamiResult,
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
RefreshResult,
|
|
37
|
+
SignOutResult,
|
|
37
38
|
SignUpInput,
|
|
38
39
|
SignInInput,
|
|
39
40
|
SignOutInput,
|
|
40
41
|
RefreshTokenInput,
|
|
41
42
|
OtpPurpose,
|
|
42
43
|
RequestOtpInput,
|
|
44
|
+
RequestOtpResult,
|
|
45
|
+
VerifyOtpResult,
|
|
43
46
|
VerifyOtpInput,
|
|
44
47
|
RequestMagicLinkInput,
|
|
48
|
+
RequestMagicLinkResult,
|
|
49
|
+
VerifyMagicLinkResult,
|
|
45
50
|
VerifyMagicLinkInput,
|
|
46
51
|
SignInCallbackResult,
|
|
47
52
|
MagicLinkCallbackResult,
|
|
@@ -49,8 +54,7 @@ export type {
|
|
|
49
54
|
VerifyJwtResult,
|
|
50
55
|
VerifyJwtFailure,
|
|
51
56
|
} from "./types.ts";
|
|
52
|
-
//
|
|
53
|
-
// (gate.identityProviders.create()).
|
|
57
|
+
// Project-scoped end-user IdP provider-config management types (gate.identityProviders.*).
|
|
54
58
|
export type {
|
|
55
59
|
IdpProviderKind,
|
|
56
60
|
IdpTierGating,
|
package/src/jwt.ts
CHANGED
|
@@ -1,58 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JWT verification primitives for
|
|
3
|
-
*
|
|
4
|
-
* Standalone-mode customers consume Gate-issued JWTs in their own
|
|
5
|
-
* application code without needing the full SDK shape — they just need
|
|
6
|
-
* to verify the signature, extract claims, and (optionally) check
|
|
7
|
-
* scopes. This module is the canonical entry for that surface.
|
|
8
|
-
*
|
|
9
|
-
* Backed by `jose` (already a peer dependency in the gateway via the
|
|
10
|
-
* same library; chose `jose` for consistency + JWK fetch support out
|
|
11
|
-
* of the box).
|
|
12
|
-
*
|
|
13
|
-
* Native-mode customers don't typically reach for this directly — the
|
|
14
|
-
* gateway verifies project-issued tokens on every request and rehydrates
|
|
15
|
-
* `subject.*` claims. But the same primitives are
|
|
16
|
-
* portable: Native customers running their own services downstream of
|
|
17
|
-
* the gateway can verify Gate-issued JWTs identically.
|
|
2
|
+
* JWT verification primitives for apps that consume Gate-issued tokens without the rest of the SDK: verify the
|
|
3
|
+
* signature, read the claims, check scopes. Backed by `jose`.
|
|
18
4
|
*/
|
|
19
5
|
|
|
20
|
-
import {
|
|
6
|
+
import { jwtVerify, errors as joseErrors } from "jose";
|
|
21
7
|
import type { GateJwtClaims, VerifyJwtFailure, VerifyJwtResult } from "./types.ts";
|
|
22
8
|
import { GateError } from "./errors.ts";
|
|
23
9
|
|
|
24
10
|
/**
|
|
25
|
-
* Configuration for `verifyJwt`.
|
|
26
|
-
*
|
|
27
|
-
* an inline key set (for tests + air-gapped deployments).
|
|
11
|
+
* Configuration for `verifyJwt`. Signing keys come from either the gateway's JWKS route (`jwksUrl`) or an inline key
|
|
12
|
+
* set (`jwks`, for tests and air-gapped deployments).
|
|
28
13
|
*/
|
|
29
14
|
export type VerifyJwtOptions = {
|
|
30
15
|
token: string;
|
|
31
16
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
17
|
+
* URL of the gateway's JWKS route: `https://<org>.api.<apex>/@<projectSlug>/auth/jwks` for a project's end-user
|
|
18
|
+
* tokens (`/auth/jwks` on the org host for org keys). The gateway wraps the key set in its response envelope
|
|
19
|
+
* (`{ data: { keys }, meta }`), which `jose` cannot read directly; this helper fetches the URL and unwraps it.
|
|
20
|
+
* Either `jwksUrl` or `jwks` must be set; not both.
|
|
34
21
|
*/
|
|
35
22
|
jwksUrl?: string;
|
|
36
|
-
/**
|
|
37
|
-
* Inline JWK set — used by tests + integration scenarios that want to
|
|
38
|
-
* avoid the network hop. The shape is the standard JWKS `{keys: [...]}`
|
|
39
|
-
* envelope; pass through unchanged to `jose`.
|
|
40
|
-
*/
|
|
23
|
+
/** Inline JWK set, the standard `{ keys: [...] }` object. */
|
|
41
24
|
jwks?: { keys: ReadonlyArray<Record<string, unknown>> };
|
|
25
|
+
/** Fetch implementation used for `jwksUrl`. Defaults to the global `fetch`. */
|
|
26
|
+
fetch?: typeof globalThis.fetch;
|
|
42
27
|
/** Expected issuer (`iss` claim). Strict comparison. */
|
|
43
28
|
expectedIssuer?: string;
|
|
44
|
-
/** Expected audience (`aud` claim).
|
|
29
|
+
/** Expected audience (`aud` claim). */
|
|
45
30
|
expectedAudience?: string;
|
|
46
31
|
/**
|
|
47
|
-
* Allowed signing algorithms. Defaults to `["RS256", "ES256"]
|
|
48
|
-
*
|
|
49
|
-
* (defense against alg-confusion attacks).
|
|
32
|
+
* Allowed signing algorithms. Defaults to `["RS256", "ES256"]`. Anything outside this set is rejected (defense
|
|
33
|
+
* against algorithm confusion).
|
|
50
34
|
*/
|
|
51
35
|
allowedAlgorithms?: ReadonlyArray<string>;
|
|
52
|
-
/**
|
|
53
|
-
* Clock-skew tolerance in seconds. Defaults to 30. Applies to both
|
|
54
|
-
* `exp` and `nbf` checks.
|
|
55
|
-
*/
|
|
36
|
+
/** Clock-skew tolerance in seconds. Defaults to 30. Applies to `exp` and `nbf`. */
|
|
56
37
|
clockToleranceSeconds?: number;
|
|
57
38
|
};
|
|
58
39
|
|
|
@@ -65,7 +46,7 @@ const DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
|
|
|
65
46
|
*
|
|
66
47
|
* Throws `GateError` only when:
|
|
67
48
|
* - `jwksUrl` and `jwks` are both unset or both set
|
|
68
|
-
* - the JWKS fetch fails
|
|
49
|
+
* - the JWKS fetch fails (UPSTREAM_UNAVAILABLE) or answers something that is not a key set
|
|
69
50
|
*/
|
|
70
51
|
export async function verifyJwt(opts: VerifyJwtOptions): Promise<VerifyJwtResult> {
|
|
71
52
|
if (opts.jwksUrl !== undefined && opts.jwks !== undefined) {
|
|
@@ -90,10 +71,8 @@ export async function verifyJwt(opts: VerifyJwtOptions): Promise<VerifyJwtResult
|
|
|
90
71
|
|
|
91
72
|
let keyResolver: Parameters<typeof jwtVerify>[1];
|
|
92
73
|
if (opts.jwksUrl !== undefined) {
|
|
93
|
-
keyResolver =
|
|
74
|
+
keyResolver = makeLocalJwksResolver(await fetchJwks(opts.jwksUrl, opts.fetch));
|
|
94
75
|
} else {
|
|
95
|
-
// Inline JWKS — implement the resolver inline since `jose`'s
|
|
96
|
-
// `createLocalJWKSet` import path varies across versions.
|
|
97
76
|
keyResolver = makeLocalJwksResolver(opts.jwks!);
|
|
98
77
|
}
|
|
99
78
|
|
|
@@ -167,6 +146,50 @@ export function requireScope(
|
|
|
167
146
|
// Internals
|
|
168
147
|
// ----------------------------------------------------------------------
|
|
169
148
|
|
|
149
|
+
type Jwks = { keys: ReadonlyArray<Record<string, unknown>> };
|
|
150
|
+
|
|
151
|
+
function isJwks(v: unknown): v is Jwks {
|
|
152
|
+
return v !== null && typeof v === "object" && Array.isArray((v as { keys?: unknown }).keys);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetch a JWKS from the gateway. The route answers `{ data: { keys }, meta }`; a bare `{ keys }` (any other JWKS
|
|
157
|
+
* host) is accepted too.
|
|
158
|
+
*/
|
|
159
|
+
export async function fetchJwks(url: string, fetchImpl: typeof globalThis.fetch | undefined = globalThis.fetch): Promise<Jwks> {
|
|
160
|
+
if (typeof fetchImpl !== "function") {
|
|
161
|
+
throw new GateError({
|
|
162
|
+
code: "INTERNAL_ERROR",
|
|
163
|
+
message: "No fetch implementation available to read the JWKS",
|
|
164
|
+
context: { url },
|
|
165
|
+
recommendation: null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
let body: unknown;
|
|
169
|
+
let status: number;
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetchImpl(url, { method: "GET", headers: { accept: "application/json" } });
|
|
172
|
+
status = res.status;
|
|
173
|
+
body = await res.json();
|
|
174
|
+
} catch (e) {
|
|
175
|
+
throw new GateError({
|
|
176
|
+
code: "UPSTREAM_UNAVAILABLE",
|
|
177
|
+
message: `Could not read the JWKS at ${url}: ${e instanceof Error ? e.message : String(e)}`,
|
|
178
|
+
context: { url },
|
|
179
|
+
recommendation: null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const inner = body !== null && typeof body === "object" ? (body as { data?: unknown }).data : undefined;
|
|
183
|
+
if (isJwks(inner)) return inner;
|
|
184
|
+
if (isJwks(body)) return body;
|
|
185
|
+
throw new GateError({
|
|
186
|
+
code: "UPSTREAM_UNAVAILABLE",
|
|
187
|
+
message: `The JWKS route at ${url} answered ${status} without a key set`,
|
|
188
|
+
context: { url, status },
|
|
189
|
+
recommendation: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
170
193
|
function classifyJoseFailure(e: unknown): VerifyJwtFailure {
|
|
171
194
|
// Map `jose`'s typed errors to our closed reason set per `VerifyJwtFailure`.
|
|
172
195
|
if (e instanceof joseErrors.JWSSignatureVerificationFailed) return "INVALID_SIGNATURE";
|
|
@@ -197,12 +220,7 @@ function base64UrlDecode(input: string): string {
|
|
|
197
220
|
return new TextDecoder("utf-8").decode(bytes);
|
|
198
221
|
}
|
|
199
222
|
|
|
200
|
-
/**
|
|
201
|
-
* Build a key resolver compatible with `jose.jwtVerify` from an inline
|
|
202
|
-
* JWKS object. `jose` exports `createLocalJWKSet` from its main module
|
|
203
|
-
* but the import path / type shape can shift across minors; reimplement
|
|
204
|
-
* the minimal resolver here for stability.
|
|
205
|
-
*/
|
|
223
|
+
/** Build a key resolver for `jose.jwtVerify` from a JWKS object. */
|
|
206
224
|
function makeLocalJwksResolver(
|
|
207
225
|
jwks: { keys: ReadonlyArray<Record<string, unknown>> },
|
|
208
226
|
): Parameters<typeof jwtVerify>[1] {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `gate.magicLinkRedirects` sub-namespace — the per-project magic-link
|
|
3
|
-
* redirect-URL allowlist, PROJECT_ADMIN+ managed.
|
|
4
|
-
* 2026-07-16): `list()`/`add()`/`remove()`, mirroring the
|
|
3
|
+
* redirect-URL allowlist, PROJECT_ADMIN+ managed. `list()`/`add()`/`remove()`, mirroring the
|
|
5
4
|
* `gate.idpProviders.*` sub-namespace convention (gate-admin surface,
|
|
6
5
|
* not the end-user-facing `passwordless.*` send/verify flow).
|
|
7
6
|
*
|