@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/src/gate.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marquee `gate.*` namespace.
|
|
3
|
+
*
|
|
4
|
+
* surfaces (canonical slug routing — `<orgSlug>.api.k9o.io/@<projectSlug>/...`):
|
|
5
|
+
* - `gate.signUp` — POST /@<projectSlug>/gate/signup
|
|
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}`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { getGateConfig, type GateConfig } from "./mode.ts";
|
|
31
|
+
import { requestWith } from "./client.ts";
|
|
32
|
+
import { resolveProjectPathRoot } from "./paths.ts";
|
|
33
|
+
import { makeIdentityProviders } from "./idp-providers/identity-providers.ts";
|
|
34
|
+
import { makeMagicLinkRedirects } from "./magic-link-redirects.ts";
|
|
35
|
+
import type {
|
|
36
|
+
AuthResult,
|
|
37
|
+
Envelope,
|
|
38
|
+
HostedGateInfo,
|
|
39
|
+
MagicLinkCallbackResult,
|
|
40
|
+
RefreshTokenInput,
|
|
41
|
+
RequestMagicLinkInput,
|
|
42
|
+
RequestOtpInput,
|
|
43
|
+
SignInCallbackResult,
|
|
44
|
+
SignInInput,
|
|
45
|
+
SignOutInput,
|
|
46
|
+
SignUpInput,
|
|
47
|
+
VerifyMagicLinkInput,
|
|
48
|
+
VerifyOtpInput,
|
|
49
|
+
VerifySessionResult,
|
|
50
|
+
WhoamiResult,
|
|
51
|
+
} from "./types.ts";
|
|
52
|
+
|
|
53
|
+
function gatePath(config: GateConfig, suffix: string): string {
|
|
54
|
+
return `${resolveProjectPathRoot(config)}/gate/${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Factory — produces a `gate.*` namespace bound to the supplied config getter.
|
|
58
|
+
* Used by both the module-level `gate` singleton (config from `configureGate`)
|
|
59
|
+
* and `createGateClient(config)` (config supplied at construction time). */
|
|
60
|
+
export function makeGateNamespace(getConfig: () => GateConfig) {
|
|
61
|
+
return {
|
|
62
|
+
/**
|
|
63
|
+
* Create a new end-user identity for the configured project. Returns
|
|
64
|
+
* the new user record + an initial session. Emits a
|
|
65
|
+
* `data.write.auth.signup` audit event.
|
|
66
|
+
*
|
|
67
|
+
* Errors (closed taxonomy):
|
|
68
|
+
* - `EMAIL_ALREADY_REGISTERED` (409) when the email collides
|
|
69
|
+
* - `INVALID_FIELD_VALUE` (400) on schema violation (password policy, malformed email)
|
|
70
|
+
* - `TIER_FEATURE_GATED` (403) when the project's tier blocks email/password
|
|
71
|
+
*/
|
|
72
|
+
async signUp(input: SignUpInput): Promise<Envelope<AuthResult>> {
|
|
73
|
+
return requestWith<AuthResult>(getConfig(), {
|
|
74
|
+
method: "POST",
|
|
75
|
+
path: gatePath(getConfig(), "signup"),
|
|
76
|
+
body: input,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sign in an existing end-user with email + password. Returns the
|
|
82
|
+
* user record + a fresh session. Emits a `data.write.auth.signin`
|
|
83
|
+
* audit event.
|
|
84
|
+
*
|
|
85
|
+
* Errors:
|
|
86
|
+
* - `INVALID_CREDENTIALS` (401) on bad email/password (ambiguous on
|
|
87
|
+
* purpose — does not distinguish "user not found" vs "password
|
|
88
|
+
* wrong" to avoid enumeration)
|
|
89
|
+
*/
|
|
90
|
+
async signIn(input: SignInInput): Promise<Envelope<AuthResult>> {
|
|
91
|
+
return requestWith<AuthResult>(getConfig(), {
|
|
92
|
+
method: "POST",
|
|
93
|
+
path: gatePath(getConfig(), "signin"),
|
|
94
|
+
body: input,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sign out — revoke the bound refresh token (or an explicit one, if
|
|
100
|
+
* supplied). Emits a `data.write.auth.signout` audit event.
|
|
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
|
|
106
|
+
*/
|
|
107
|
+
async signOut(input: SignOutInput = {}): Promise<Envelope<{ revoked: boolean }>> {
|
|
108
|
+
return requestWith<{ revoked: boolean }>(getConfig(), {
|
|
109
|
+
method: "POST",
|
|
110
|
+
path: gatePath(getConfig(), "signout"),
|
|
111
|
+
body: input,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Return the caller's identity claims from the bearer JWT. Useful in
|
|
117
|
+
* Standalone mode where the SDK consumer wants to surface the user's
|
|
118
|
+
* roles / groups without parsing the JWT itself.
|
|
119
|
+
*
|
|
120
|
+
* Errors:
|
|
121
|
+
* - `UNAUTHENTICATED` (401) when no bearer is present
|
|
122
|
+
* - `SESSION_EXPIRED` (401) when the bearer's exp has passed
|
|
123
|
+
*/
|
|
124
|
+
async whoami(): Promise<Envelope<WhoamiResult>> {
|
|
125
|
+
return requestWith<WhoamiResult>(getConfig(), {
|
|
126
|
+
method: "GET",
|
|
127
|
+
path: gatePath(getConfig(), "whoami"),
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Rotate the access token using a refresh token. Returns a fresh
|
|
133
|
+
* session pair (new access token + same refresh token; gateway may
|
|
134
|
+
* also rotate the refresh token per project policy).
|
|
135
|
+
*
|
|
136
|
+
* Errors:
|
|
137
|
+
* - `REFRESH_TOKEN_INVALID` (400) when the refresh token is unknown,
|
|
138
|
+
* expired, or revoked
|
|
139
|
+
*/
|
|
140
|
+
async refreshToken(input: RefreshTokenInput): Promise<Envelope<{ session: AuthResult["session"] }>> {
|
|
141
|
+
return requestWith<{ session: AuthResult["session"] }>(getConfig(), {
|
|
142
|
+
method: "POST",
|
|
143
|
+
path: gatePath(getConfig(), "refresh"),
|
|
144
|
+
body: input,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
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
|
+
/**
|
|
176
|
+
* Initiate an external-IdP sign-in flow (Google / GitHub / Microsoft).
|
|
177
|
+
* The SDK POSTs to the gateway's authorize endpoint, receives the
|
|
178
|
+
* provider's authorization URL with a signed-state JWT (carrying PKCE
|
|
179
|
+
* verifier + the supplied `redirectUri`), and redirects the browser.
|
|
180
|
+
*
|
|
181
|
+
* After the user authenticates with the IdP, the gateway processes the
|
|
182
|
+
* callback and redirects the browser back to `redirectUri` with session
|
|
183
|
+
* tokens in the URL fragment. Use `parseSignInCallback()` on the
|
|
184
|
+
* destination page to extract them.
|
|
185
|
+
*
|
|
186
|
+
* Example:
|
|
187
|
+
* await gate.signInWithProvider({
|
|
188
|
+
* provider: 'google',
|
|
189
|
+
* redirectUri: 'https://app.example.com/auth/callback',
|
|
190
|
+
* });
|
|
191
|
+
* // …user lands at /auth/callback#access_token=…&refresh_token=…
|
|
192
|
+
*
|
|
193
|
+
* Generic OIDC requires authorization-endpoint discovery and is not
|
|
194
|
+
* yet wired; use a hosted provider (google/github/microsoft) for now.
|
|
195
|
+
*/
|
|
196
|
+
async signInWithProvider(input: {
|
|
197
|
+
provider: "google" | "github" | "microsoft" | "oidc" | "saml";
|
|
198
|
+
redirectUri: string;
|
|
199
|
+
}): Promise<void> {
|
|
200
|
+
const env = await requestWith<{ authorizationUrl: string }>(getConfig(), {
|
|
201
|
+
method: "POST",
|
|
202
|
+
path: `${resolveProjectPathRoot(getConfig())}/gate/idp/${input.provider}/authorize`,
|
|
203
|
+
body: { redirectUri: input.redirectUri },
|
|
204
|
+
});
|
|
205
|
+
const w = (globalThis as { window?: { location: { assign: (url: string) => void } } }).window;
|
|
206
|
+
if (w !== undefined) {
|
|
207
|
+
w.location.assign(env.data.authorizationUrl);
|
|
208
|
+
} else {
|
|
209
|
+
throw new Error(
|
|
210
|
+
"signInWithProvider requires a browser environment (window.location); for server-side initiation, fetch the authorize endpoint directly.",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse session tokens out of the URL fragment delivered by an
|
|
217
|
+
* IdP callback redirect. Returns the session payload on success or an
|
|
218
|
+
* error object on failure. Pass `window.location.hash` (or any
|
|
219
|
+
* `#`-prefixed string) — leading `#` is stripped automatically.
|
|
220
|
+
*
|
|
221
|
+
* Side-effect-free: does not call the gateway and does not mutate
|
|
222
|
+
* any session storage. The consumer is responsible for persisting
|
|
223
|
+
* the returned tokens (e.g. via the app's auth context).
|
|
224
|
+
*/
|
|
225
|
+
parseSignInCallback(hash: string): SignInCallbackResult {
|
|
226
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
227
|
+
const params = new URLSearchParams(raw);
|
|
228
|
+
const error = params.get("error");
|
|
229
|
+
if (error !== null) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
error,
|
|
233
|
+
errorDescription: params.get("error_description") ?? error,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const accessToken = params.get("access_token");
|
|
237
|
+
const refreshToken = params.get("refresh_token");
|
|
238
|
+
const expiresAt = params.get("expires_at");
|
|
239
|
+
if (accessToken === null || refreshToken === null || expiresAt === null) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "malformed_callback",
|
|
243
|
+
errorDescription:
|
|
244
|
+
"Callback URL fragment missing one or more required fields (access_token, refresh_token, expires_at).",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
accessToken,
|
|
250
|
+
refreshToken,
|
|
251
|
+
expiresAt,
|
|
252
|
+
tokenType: params.get("token_type") ?? "Bearer",
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Passwordless sub-namespace — OTP + Magic Link flows
|
|
258
|
+
* for email-based authentication without a stored password.
|
|
259
|
+
*
|
|
260
|
+
* Wire endpoints (LIVE on the Gate end-user plane — verified against
|
|
261
|
+
* staging): OTP → `POST /@<projectSlug>/gate/otp/{request,verify}`
|
|
262
|
+
* (routes/otp.ts), Magic Link → `POST /@<projectSlug>/gate/magic-link/
|
|
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.)
|
|
267
|
+
*/
|
|
268
|
+
passwordless: {
|
|
269
|
+
/**
|
|
270
|
+
* Request a one-time passcode delivered to the supplied email.
|
|
271
|
+
* The OTP is short-lived (typically 5 min) and
|
|
272
|
+
* single-use; redemption via `verifyOtp` mints a session.
|
|
273
|
+
*/
|
|
274
|
+
async requestOtp(input: RequestOtpInput): Promise<Envelope<{ requested: boolean }>> {
|
|
275
|
+
return requestWith<{ requested: boolean }>(getConfig(), {
|
|
276
|
+
method: "POST",
|
|
277
|
+
path: gatePath(getConfig(), "otp/request"),
|
|
278
|
+
body: input,
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Verify a one-time passcode previously sent via `requestOtp`.
|
|
284
|
+
* On success returns the same `AuthResult` shape as `signIn` —
|
|
285
|
+
* a fresh session bound to the end-user identity.
|
|
286
|
+
*
|
|
287
|
+
* Errors:
|
|
288
|
+
* - INVALID_CREDENTIALS (401) on wrong / expired / consumed OTP
|
|
289
|
+
* (ambiguous on purpose — enumeration defense)
|
|
290
|
+
*/
|
|
291
|
+
async verifyOtp(input: VerifyOtpInput): Promise<Envelope<AuthResult>> {
|
|
292
|
+
return requestWith<AuthResult>(getConfig(), {
|
|
293
|
+
method: "POST",
|
|
294
|
+
path: gatePath(getConfig(), "otp/verify"),
|
|
295
|
+
body: input,
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Request a magic-link emailed to the supplied address. The link
|
|
301
|
+
* carries an opaque short-lived token (typically 15 min
|
|
302
|
+
*); clicking the link lands at the customer's chosen
|
|
303
|
+
* redirect URL with the token in the query string, which the
|
|
304
|
+
* customer's frontend then exchanges via `verifyMagicLink`.
|
|
305
|
+
*/
|
|
306
|
+
async requestMagicLink(
|
|
307
|
+
input: RequestMagicLinkInput,
|
|
308
|
+
): Promise<Envelope<{ requested: boolean }>> {
|
|
309
|
+
return requestWith<{ requested: boolean }>(getConfig(), {
|
|
310
|
+
method: "POST",
|
|
311
|
+
path: gatePath(getConfig(), "magic-link/send"),
|
|
312
|
+
body: input,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Verify a magic-link token previously delivered via
|
|
318
|
+
* `requestMagicLink`. On success returns the same `AuthResult`
|
|
319
|
+
* shape as `signIn`.
|
|
320
|
+
*
|
|
321
|
+
* Errors:
|
|
322
|
+
* - INVALID_CREDENTIALS (401) on wrong / expired / consumed
|
|
323
|
+
* token (same enumeration-defense rationale as verifyOtp)
|
|
324
|
+
*/
|
|
325
|
+
async verifyMagicLink(input: VerifyMagicLinkInput): Promise<Envelope<AuthResult>> {
|
|
326
|
+
return requestWith<AuthResult>(getConfig(), {
|
|
327
|
+
method: "POST",
|
|
328
|
+
path: gatePath(getConfig(), "magic-link/redeem"),
|
|
329
|
+
body: input,
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Extract the opaque magic-link token from the landing-page URL's query
|
|
335
|
+
* string (the gateway redirects to `<configured-redirect>?token=…&project=…`).
|
|
336
|
+
* Pure + side-effect-free — pass `window.location.search`. Feed the
|
|
337
|
+
* returned `token` to `verifyMagicLink({ token })` to mint the session.
|
|
338
|
+
*
|
|
339
|
+
* This is the magic-link counterpart to `parseSignInCallback` (which parses
|
|
340
|
+
* the SSO *hash fragment* for session tokens) — magic-link delivers an
|
|
341
|
+
* opaque token in the *query string*, so the two are not interchangeable.
|
|
342
|
+
*/
|
|
343
|
+
parseMagicLinkCallback(search: string): MagicLinkCallbackResult {
|
|
344
|
+
const raw = search.startsWith("?") ? search.slice(1) : search;
|
|
345
|
+
const params = new URLSearchParams(raw);
|
|
346
|
+
const error = params.get("error");
|
|
347
|
+
if (error !== null) {
|
|
348
|
+
return { ok: false, error };
|
|
349
|
+
}
|
|
350
|
+
const token = params.get("token");
|
|
351
|
+
if (token === null || token.length === 0) {
|
|
352
|
+
return { ok: false, error: "missing_token" };
|
|
353
|
+
}
|
|
354
|
+
return { ok: true, token };
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Project-scoped end-user IdP provider-config management.
|
|
360
|
+
* PROJECT_ADMIN+ required; end-user JWTs rejected at the route. Only
|
|
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.
|
|
364
|
+
*/
|
|
365
|
+
identityProviders: makeIdentityProviders(getConfig),
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Per-project magic-link redirect-URL allowlist management (blessed
|
|
369
|
+
* shape). Gate-admin surface alongside `identityProviders`
|
|
370
|
+
* — PROJECT_ADMIN+ required, distinct from the end-user-facing
|
|
371
|
+
* `passwordless.requestMagicLink`/`verifyMagicLink` send/verify flow.
|
|
372
|
+
*/
|
|
373
|
+
magicLinkRedirects: makeMagicLinkRedirects(getConfig),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Singleton bound to the module-level `configureGate()` config.
|
|
378
|
+
* Per-instance clients: `createGateClient(config)`. */
|
|
379
|
+
export const gate = makeGateNamespace(getGateConfig);
|
|
380
|
+
|
|
381
|
+
/** Convenience type alias mirroring the `gate` runtime export.
|
|
382
|
+
* Identical shape as the per-instance `GateClient` returned from
|
|
383
|
+
* `createGateClient`. */
|
|
384
|
+
export type GateNamespace = ReturnType<typeof makeGateNamespace>;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gate.identityProviders` namespace — project-scoped end-user IdP
|
|
3
|
+
* provider-config management.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the live gateway route family shipped in
|
|
6
|
+
* `services/gateway/src/routes/project-identity-providers.ts`, mounted on
|
|
7
|
+
* the project plane (`<orgSlug>.api.k9o.io/@<projectSlug>/identity-providers`):
|
|
8
|
+
*
|
|
9
|
+
* POST /@<projectSlug>/identity-providers — create (PROJECT_ADMIN)
|
|
10
|
+
* GET /@<projectSlug>/identity-providers — list (READ_ROLES)
|
|
11
|
+
* PATCH /@<projectSlug>/identity-providers/:epcId — update (PROJECT_ADMIN)
|
|
12
|
+
* POST /@<projectSlug>/identity-providers/:epcId/test — testProvider (PROJECT_ADMIN)
|
|
13
|
+
*
|
|
14
|
+
* `create()` + `list()` back the Gate Studio `FederationPresetsPanel`; `update()` backs its
|
|
15
|
+
* `DirectorySyncPanel` (directory-sync settings are read off `list()` and written via `update()`, replacing
|
|
16
|
+
* the unanswered `federation.getDirectorySync`/`updateDirectorySync` bridge messages). Read-one/delete stay
|
|
17
|
+
* unbound until a caller needs them (the SDK does not speculatively surface routes).
|
|
18
|
+
*
|
|
19
|
+
* Auth: operator JWT with PROJECT_ADMIN+ role, server-enforced by the
|
|
20
|
+
* gateway RBAC layer (`requireProjectAdmin`); end-user JWTs (`eu_`-prefixed
|
|
21
|
+
* subjects) are rejected at the route. The bearer is taken from
|
|
22
|
+
* `partnerToken` in config, or per-call via the client's bearer plumbing.
|
|
23
|
+
*
|
|
24
|
+
* 🔴 `clientSecretRef` is a RAW SECRET on the way in, not a reference.
|
|
25
|
+
* Post-v105 the gateway encrypts the submitted value (AES-256-GCM under
|
|
26
|
+
* `K9O_SECRET_STORAGE_MASTER_KEY`) into the opaque `client_secret_enc`
|
|
27
|
+
* handle and persists only that; the field name is retained for wire
|
|
28
|
+
* compatibility but the create handler comment
|
|
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
|
|
31
|
+
* handle: they carry only `clientSecretSet` (a presence boolean,
|
|
32
|
+
* the write-only fix). `serviceAccountKey` (directory sync) follows the same write-only rule.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { getGateConfig, type GateConfig } from "../mode.ts";
|
|
36
|
+
import { requestWith } from "../client.ts";
|
|
37
|
+
import { resolveProjectPathRoot } from "../paths.ts";
|
|
38
|
+
import type { Envelope } from "../types.ts";
|
|
39
|
+
|
|
40
|
+
/** Closed end-user-class provider enum — 11 values since v221 (the project-plane substrate
|
|
41
|
+
* `_sys.end_user_idp_provider_configs`); the org plane carries the same 11 since v222/v223. */
|
|
42
|
+
export type IdpProviderKind =
|
|
43
|
+
| "google" | "github" | "microsoft" | "oidc" | "saml" | "scim"
|
|
44
|
+
| "ldap" | "active_directory" | "apple" | "facebook" | "twitter";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The result of a provider connectivity test (`testProvider`). `ok` is the reachability + metadata-validity
|
|
48
|
+
* verdict; `reason` (present only when `!ok`) is a CLOSED-enum machine code, NEVER the upstream response body
|
|
49
|
+
* or any credential material. v1 is a NO-SECRET probe: it validates the issuer/metadata
|
|
50
|
+
* endpoint is reachable + structurally valid; it never transmits the stored client secret. `credentials_rejected`
|
|
51
|
+
* is deliberately NOT a v1 reason — the secret is never used, so it cannot be validated (a v2 concern).
|
|
52
|
+
*/
|
|
53
|
+
export type FederationTestProviderResult = {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
reason?:
|
|
56
|
+
| "endpoint_unreachable"
|
|
57
|
+
| "endpoint_forbidden"
|
|
58
|
+
| "metadata_invalid"
|
|
59
|
+
| "secret_not_provisioned"
|
|
60
|
+
| "kind_unsupported";
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Per the provider→tier pairing is structurally fixed:
|
|
64
|
+
* `free_tier_ok ∈ {google, github, microsoft, apple, facebook, twitter}`,
|
|
65
|
+
* `ridge_plus ∈ {oidc, saml, scim, ldap, active_directory}`. The gateway rejects a mismatched
|
|
66
|
+
* pairing (`K9O_INVALID_FIELD_VALUE`); the SDK forwards the caller value
|
|
67
|
+
* verbatim rather than second-guessing it. */
|
|
68
|
+
export type IdpTierGating = "free_tier_ok" | "ridge_plus";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The provider-config metadata shape returned by every read surface and
|
|
72
|
+
* mutation response (`shapeIdpConfig`). Mirrors the gateway wire shape
|
|
73
|
+
* one-to-one.
|
|
74
|
+
*
|
|
75
|
+
* Reads carry `clientSecretSet` (presence only), never the secret or a handle (see the module header).
|
|
76
|
+
* `metadataUrl`/`updatedAt` are `null` until set; `attributeMap` defaults to `{}`; `allowedRedirectUris`
|
|
77
|
+
* defaults to `[]`. The `directory*` fields are GS-F7 scheduled directory sync (Google only,):
|
|
78
|
+
* settings plus the scheduler's last-run status; the service-account key itself is never returned.
|
|
79
|
+
*/
|
|
80
|
+
export type IdpConfig = {
|
|
81
|
+
id: string;
|
|
82
|
+
projectId: string;
|
|
83
|
+
idpProvider: IdpProviderKind;
|
|
84
|
+
displayName: string;
|
|
85
|
+
clientId: string;
|
|
86
|
+
/** Whether a client secret is stored. The secret (and any handle to it) is write-only. */
|
|
87
|
+
clientSecretSet: boolean;
|
|
88
|
+
redirectUri: string;
|
|
89
|
+
metadataUrl: string | null;
|
|
90
|
+
attributeMap: Record<string, unknown>;
|
|
91
|
+
tierGating: IdpTierGating;
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
allowedRedirectUris: string[];
|
|
94
|
+
directoryPullEnabled: boolean;
|
|
95
|
+
directorySyncSubject: string | null;
|
|
96
|
+
directorySyncIntervalMins: number | null;
|
|
97
|
+
directorySyncLastRunAt: string | null;
|
|
98
|
+
directorySyncLastStatus: string | null;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
updatedAt: string | null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** GS-F7 directory-sync write fields — accepted on create AND update (one PROJECT_ADMIN surface). */
|
|
104
|
+
export type DirectorySyncInput = {
|
|
105
|
+
/** Enabling requires `idpProvider: "google"`, a `serviceAccountKey` (now or already stored) and an interval. */
|
|
106
|
+
directoryPullEnabled?: boolean;
|
|
107
|
+
directorySyncIntervalMins?: number;
|
|
108
|
+
/** The raw Google service-account key JSON. Write-only: encrypted at rest server-side, never returned. */
|
|
109
|
+
serviceAccountKey?: string;
|
|
110
|
+
/** The Workspace admin user the service account impersonates (domain-wide delegation subject). */
|
|
111
|
+
directorySyncSubject?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create-request body — mirrors the gateway `CreateIdpBody` (t.Object)
|
|
116
|
+
* exactly, including which fields are optional.
|
|
117
|
+
*/
|
|
118
|
+
export type CreateIdpInput = {
|
|
119
|
+
idpProvider: IdpProviderKind;
|
|
120
|
+
displayName: string;
|
|
121
|
+
clientId: string;
|
|
122
|
+
/**
|
|
123
|
+
* The OAuth2 client secret. Sent over TLS; encrypted-at-rest server-side
|
|
124
|
+
* (AES-256-GCM) and never returned. Named `clientSecretRef` for wire
|
|
125
|
+
* compatibility — it is NOT a reference on the way in (see module header).
|
|
126
|
+
*/
|
|
127
|
+
clientSecretRef: string;
|
|
128
|
+
redirectUri: string;
|
|
129
|
+
/** Optional; `null`/omitted for providers that don't use discovery metadata. */
|
|
130
|
+
metadataUrl?: string | null;
|
|
131
|
+
/** Optional claim→attribute mapping; defaults to `{}` server-side. */
|
|
132
|
+
attributeMap?: Record<string, string>;
|
|
133
|
+
tierGating: IdpTierGating;
|
|
134
|
+
/** Defaults to `false` server-side — a provider is created disabled. */
|
|
135
|
+
enabled?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Customer SPA origins the SDK gate flow (`gate.signInWithProvider`) may
|
|
138
|
+
* hand session tokens back to. Origin-matched + fail-closed at authorize,
|
|
139
|
+
* so an empty/omitted list means SDK sign-in is disabled for this provider
|
|
140
|
+
* until an origin is registered. Defaults to `[]` server-side.
|
|
141
|
+
*/
|
|
142
|
+
allowedRedirectUris?: string[] | null;
|
|
143
|
+
} & DirectorySyncInput;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update-request body — mirrors the gateway `PatchIdpBody` exactly: every field optional; `idpProvider` and
|
|
147
|
+
* `tierGating` are immutable after create. An empty patch returns the current config unchanged.
|
|
148
|
+
*
|
|
149
|
+
* 🔴 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,): re-supply the
|
|
151
|
+
* secret whenever the metadata URL changes.
|
|
152
|
+
*/
|
|
153
|
+
export type PatchIdpInput = {
|
|
154
|
+
displayName?: string;
|
|
155
|
+
clientId?: string;
|
|
156
|
+
/** The raw client secret (write-only; see the module header). */
|
|
157
|
+
clientSecretRef?: string;
|
|
158
|
+
redirectUri?: string;
|
|
159
|
+
metadataUrl?: string | null;
|
|
160
|
+
attributeMap?: Record<string, string>;
|
|
161
|
+
enabled?: boolean;
|
|
162
|
+
allowedRedirectUris?: string[] | null;
|
|
163
|
+
} & DirectorySyncInput;
|
|
164
|
+
|
|
165
|
+
function identityProvidersPath(config: GateConfig): string {
|
|
166
|
+
return `${resolveProjectPathRoot(config)}/identity-providers`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Factory — produces an `identityProviders` namespace bound to the supplied
|
|
170
|
+
* config getter. Used by both the `gate` singleton + `createGateClient`. */
|
|
171
|
+
export function makeIdentityProviders(getConfig: () => GateConfig) {
|
|
172
|
+
return {
|
|
173
|
+
/**
|
|
174
|
+
* Register a new end-user IdP provider config for the configured
|
|
175
|
+
* project. PROJECT_ADMIN+ required (server-enforced). The plaintext
|
|
176
|
+
* client secret is encrypted-at-rest server-side; the returned config
|
|
177
|
+
* carries only the opaque secret handle.
|
|
178
|
+
*
|
|
179
|
+
* The gateway validates the `idpProvider`→`tierGating` pairing
|
|
180
|
+
* and rejects a mismatch with `K9O_INVALID_FIELD_VALUE`.
|
|
181
|
+
*/
|
|
182
|
+
async create(input: CreateIdpInput): Promise<Envelope<IdpConfig>> {
|
|
183
|
+
const config = getConfig();
|
|
184
|
+
return requestWith<IdpConfig>(config, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: identityProvidersPath(config),
|
|
187
|
+
body: input,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* List the configured end-user IdP provider configs for the project.
|
|
193
|
+
* READ_ROLES-gated server-side (PROJECT_ADMIN/EDITOR/MEMBER/VIEWER/AUTHOR/
|
|
194
|
+
* ANALYST) — the SAME shape + roles as the existing GET route
|
|
195
|
+
* (`listProviders` returns the existing route's shape, no
|
|
196
|
+
* confidentiality-narrower endpoint). Each `IdpConfig` carries the opaque
|
|
197
|
+
* secret handle, never the secret. Callers that only need the summary
|
|
198
|
+
* (`id`/`idpProvider`/`displayName`/`enabled`/`tierGating`) pick fields off
|
|
199
|
+
* the result — no server change needed for that shaping.
|
|
200
|
+
*/
|
|
201
|
+
async list(): Promise<Envelope<IdpConfig[]>> {
|
|
202
|
+
const config = getConfig();
|
|
203
|
+
return requestWith<IdpConfig[]>(config, {
|
|
204
|
+
method: "GET",
|
|
205
|
+
path: identityProvidersPath(config),
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Test connectivity to a configured provider by id — a NO-SECRET reachability + metadata-validity probe
|
|
211
|
+
* (no-secret): the gateway resolve-and-pins the issuer/metadata endpoint (SSRF-fenced) and
|
|
212
|
+
* validates it structurally, WITHOUT decrypting or transmitting the stored client secret. Returns
|
|
213
|
+
* `{ok, reason?}`; the `reason` is a closed machine code, never the upstream body or any credential.
|
|
214
|
+
* PROJECT_ADMIN required (server-enforced) — it triggers live external egress on the platform's behalf.
|
|
215
|
+
*/
|
|
216
|
+
/**
|
|
217
|
+
* Update a provider config by id (PATCH; PROJECT_ADMIN, server-enforced). Partial: only supplied fields
|
|
218
|
+
* change; `idpProvider`/`tierGating` are immutable. Returns the updated config (secrets never included).
|
|
219
|
+
* GS-F7: write directory-sync settings here and read their status back off `list()`. See `PatchIdpInput`
|
|
220
|
+
* for the metadataUrl-change-clears-secret rule.
|
|
221
|
+
*/
|
|
222
|
+
async update(configId: string, patch: PatchIdpInput): Promise<Envelope<IdpConfig>> {
|
|
223
|
+
const config = getConfig();
|
|
224
|
+
return requestWith<IdpConfig>(config, {
|
|
225
|
+
method: "PATCH",
|
|
226
|
+
path: `${identityProvidersPath(config)}/${encodeURIComponent(configId)}`,
|
|
227
|
+
body: patch,
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async testProvider(configId: string): Promise<Envelope<FederationTestProviderResult>> {
|
|
232
|
+
const config = getConfig();
|
|
233
|
+
return requestWith<FederationTestProviderResult>(config, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
path: `${identityProvidersPath(config)}/${encodeURIComponent(configId)}/test`,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Singleton bound to the module-level `configureGate()` config. */
|
|
242
|
+
export const identityProviders = makeIdentityProviders(getGateConfig);
|
|
243
|
+
|
|
244
|
+
export type IdentityProvidersNamespace = ReturnType<typeof makeIdentityProviders>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@k9o/gate` — Gate (Identity-Aware Kernel) SDK entry point.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the canonical surface +:
|
|
5
|
+
* - `gate` — marquee namespace + sub-namespaces
|
|
6
|
+
* - `configureGate` / `getGateConfig` / `resetGateConfig` — mode + config plumbing
|
|
7
|
+
* - `verifyJwt` / `extractClaims` / `requireScope` — standalone JWT primitives
|
|
8
|
+
* - `GateError` + `GateErrorCode` — KError-compatible error surface
|
|
9
|
+
* - All public types via `* from "./types.ts"`
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { gate, type GateNamespace } from "./gate.ts";
|
|
13
|
+
export { createGateClient, type GateClient } from "./client-factory.ts";
|
|
14
|
+
export { resolveRegionalOrigin, regionalOriginFromLocation, type RegionConfig } from "./region.ts";
|
|
15
|
+
export {
|
|
16
|
+
configureGate,
|
|
17
|
+
getGateConfig,
|
|
18
|
+
resetGateConfig,
|
|
19
|
+
type GateConfig,
|
|
20
|
+
type GateMode,
|
|
21
|
+
} from "./mode.ts";
|
|
22
|
+
export { verifyJwt, extractClaims, requireScope, type VerifyJwtOptions } from "./jwt.ts";
|
|
23
|
+
export {
|
|
24
|
+
GateError,
|
|
25
|
+
type GateErrorCode,
|
|
26
|
+
type KErrorWire,
|
|
27
|
+
gateErrorFromWire,
|
|
28
|
+
} from "./errors.ts";
|
|
29
|
+
export type {
|
|
30
|
+
Envelope,
|
|
31
|
+
EndUser,
|
|
32
|
+
Session,
|
|
33
|
+
AuthResult,
|
|
34
|
+
WhoamiResult,
|
|
35
|
+
VerifySessionResult,
|
|
36
|
+
HostedGateInfo,
|
|
37
|
+
SignUpInput,
|
|
38
|
+
SignInInput,
|
|
39
|
+
SignOutInput,
|
|
40
|
+
RefreshTokenInput,
|
|
41
|
+
OtpPurpose,
|
|
42
|
+
RequestOtpInput,
|
|
43
|
+
VerifyOtpInput,
|
|
44
|
+
RequestMagicLinkInput,
|
|
45
|
+
VerifyMagicLinkInput,
|
|
46
|
+
SignInCallbackResult,
|
|
47
|
+
MagicLinkCallbackResult,
|
|
48
|
+
GateJwtClaims,
|
|
49
|
+
VerifyJwtResult,
|
|
50
|
+
VerifyJwtFailure,
|
|
51
|
+
} from "./types.ts";
|
|
52
|
+
// GS-F6 — project-scoped end-user IdP provider-config management types
|
|
53
|
+
// (gate.identityProviders.create()).
|
|
54
|
+
export type {
|
|
55
|
+
IdpProviderKind,
|
|
56
|
+
IdpTierGating,
|
|
57
|
+
IdpConfig,
|
|
58
|
+
CreateIdpInput,
|
|
59
|
+
PatchIdpInput,
|
|
60
|
+
DirectorySyncInput,
|
|
61
|
+
IdentityProvidersNamespace,
|
|
62
|
+
} from "./idp-providers/identity-providers.ts";
|