@cosmicdrift/kumiko-framework 0.102.2 → 0.104.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/package.json +2 -2
- package/src/api/auth-routes.ts +22 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"zod": "^4.4.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
184
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.104.0",
|
|
185
185
|
"bun-types": "^1.3.13",
|
|
186
186
|
"pino-pretty": "^13.1.3"
|
|
187
187
|
},
|
package/src/api/auth-routes.ts
CHANGED
|
@@ -299,33 +299,17 @@ export type AuthRoutesConfig = {
|
|
|
299
299
|
|
|
300
300
|
export type PasswordResetConfig = {
|
|
301
301
|
// Qualified name of the request handler (the one that emits either
|
|
302
|
-
// { kind: "reset-requested", ... } or { kind: "no-op" }).
|
|
302
|
+
// { kind: "reset-requested", ... } or { kind: "no-op" }). The handler
|
|
303
|
+
// builds the magic-link and sends the mail via delivery (ctx.notify) — the
|
|
304
|
+
// route only dispatches and returns the silent-success envelope.
|
|
303
305
|
requestHandler: string;
|
|
304
306
|
// Qualified name of the confirm handler (token + newPassword → set).
|
|
305
307
|
confirmHandler: string;
|
|
306
|
-
// Invoked only when the request handler returns kind=reset-requested.
|
|
307
|
-
// Given the signed token + target email, the callback builds the URL
|
|
308
|
-
// into the caller's app and hands it to whatever delivery channel the
|
|
309
|
-
// app wires up. Errors bubble as 5xx so silent drop-on-send can't hide
|
|
310
|
-
// an outgoing-mail outage behind a green response.
|
|
311
|
-
sendResetEmail: (args: { email: string; resetUrl: string; expiresAt: string }) => Promise<void>;
|
|
312
|
-
// Base URL of the app that hosts the reset form. The route appends
|
|
313
|
-
// `?token=…` so you should NOT include a trailing `?` or `#`. Example:
|
|
314
|
-
// "https://app.example.com/reset-password"
|
|
315
|
-
appResetUrl: string;
|
|
316
308
|
};
|
|
317
309
|
|
|
318
310
|
export type EmailVerificationConfig = {
|
|
319
311
|
requestHandler: string;
|
|
320
312
|
confirmHandler: string;
|
|
321
|
-
sendVerificationEmail: (args: {
|
|
322
|
-
email: string;
|
|
323
|
-
verificationUrl: string;
|
|
324
|
-
expiresAt: string;
|
|
325
|
-
}) => Promise<void>;
|
|
326
|
-
// URL of the app page that receives the `?token=…` parameter and POSTs
|
|
327
|
-
// it to /auth/verify-email on submit.
|
|
328
|
-
appVerifyUrl: string;
|
|
329
313
|
};
|
|
330
314
|
|
|
331
315
|
// Tenant-Invite Magic-Link. Drei Accept-Branches für klare Separation:
|
|
@@ -334,28 +318,20 @@ export type EmailVerificationConfig = {
|
|
|
334
318
|
// - signupCompleteHandler: anon User mit neuer email (Branch 3)
|
|
335
319
|
// Branch 2+3 minten JWT analog signup-confirm.
|
|
336
320
|
export type InviteConfig = {
|
|
337
|
-
// Qualified handler names
|
|
321
|
+
// Qualified handler names. invite-create dispatches the invite mail itself
|
|
322
|
+
// via delivery (ctx.notify); the route layer only wires the accept branches.
|
|
338
323
|
readonly acceptHandler: string;
|
|
339
324
|
readonly acceptWithLoginHandler: string;
|
|
340
325
|
readonly signupCompleteHandler: string;
|
|
341
|
-
// Mail-Callback. Token-URL wird von der App-Page (z.B. /invite/accept)
|
|
342
|
-
// an den User geschickt; der Frontend leitet je nach User-State (eingeloggt
|
|
343
|
-
// / anon mit existing-email / anon mit neuer email) auf den passenden
|
|
344
|
-
// Branch-Endpoint.
|
|
345
|
-
readonly sendInviteEmail: (args: {
|
|
346
|
-
email: string;
|
|
347
|
-
inviteUrl: string;
|
|
348
|
-
expiresAt: string;
|
|
349
|
-
role: string;
|
|
350
|
-
}) => Promise<void>;
|
|
351
|
-
readonly appAcceptUrl: string;
|
|
352
326
|
};
|
|
353
327
|
|
|
354
328
|
// Magic-Link Self-Signup. Anders als reset/verify NICHT HMAC-signed —
|
|
355
|
-
// der Token ist opaque random, Redis ist Source of Truth.
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
329
|
+
// der Token ist opaque random, Redis ist Source of Truth. The request
|
|
330
|
+
// handler dispatches the activation mail via delivery (ctx.notify); the
|
|
331
|
+
// route only forwards the dispatch. Confirm returnt
|
|
332
|
+
// `{ kind: "auth-session", session, tenantKey }` analog zu loginHandler,
|
|
333
|
+
// sodass die Route JWT minten + Cookies setzen kann (Auto-Login direkt
|
|
334
|
+
// nach Activation, kein zweiter login-Roundtrip).
|
|
359
335
|
export type SignupConfig = {
|
|
360
336
|
// Qualified name of the request handler (typisch
|
|
361
337
|
// AuthHandlers.signupRequest).
|
|
@@ -364,16 +340,6 @@ export type SignupConfig = {
|
|
|
364
340
|
// AuthHandlers.signupConfirm). Returnt SessionUser-Shape — die
|
|
365
341
|
// Route wickelt das wie einen erfolgreichen login.
|
|
366
342
|
confirmHandler: string;
|
|
367
|
-
// Mail-Callback. Token-URL wird als `${appActivationUrl}?token=…`
|
|
368
|
-
// an die App-Page geleitet.
|
|
369
|
-
sendActivationEmail: (args: {
|
|
370
|
-
email: string;
|
|
371
|
-
activationUrl: string;
|
|
372
|
-
expiresAt: string;
|
|
373
|
-
}) => Promise<void>;
|
|
374
|
-
// Base URL of the app page that receives the `?token=…` parameter
|
|
375
|
-
// (typisch /signup/complete). KEIN trailing `?` oder `#`.
|
|
376
|
-
appActivationUrl: string;
|
|
377
343
|
};
|
|
378
344
|
|
|
379
345
|
// Extract `ip` and `user-agent` for the sessionCreator.
|
|
@@ -560,10 +526,6 @@ export function createAuthRoutes(
|
|
|
560
526
|
dispatcher,
|
|
561
527
|
path: Routes.authRequestPasswordReset,
|
|
562
528
|
requestHandler: pr.requestHandler,
|
|
563
|
-
successKind: "reset-requested",
|
|
564
|
-
appBaseUrl: pr.appResetUrl,
|
|
565
|
-
sendEmail: ({ email, url, expiresAt }) =>
|
|
566
|
-
pr.sendResetEmail({ email, resetUrl: url, expiresAt }), // @wrapper-known semantic-alias
|
|
567
529
|
});
|
|
568
530
|
registerTokenConfirmRoute({
|
|
569
531
|
api,
|
|
@@ -582,10 +544,6 @@ export function createAuthRoutes(
|
|
|
582
544
|
dispatcher,
|
|
583
545
|
path: Routes.authRequestEmailVerification,
|
|
584
546
|
requestHandler: ev.requestHandler,
|
|
585
|
-
successKind: "verification-requested",
|
|
586
|
-
appBaseUrl: ev.appVerifyUrl,
|
|
587
|
-
sendEmail: ({ email, url, expiresAt }) =>
|
|
588
|
-
ev.sendVerificationEmail({ email, verificationUrl: url, expiresAt }), // @wrapper-known semantic-alias
|
|
589
547
|
});
|
|
590
548
|
registerTokenConfirmRoute({
|
|
591
549
|
api,
|
|
@@ -607,10 +565,6 @@ export function createAuthRoutes(
|
|
|
607
565
|
dispatcher,
|
|
608
566
|
path: Routes.authSignupRequest,
|
|
609
567
|
requestHandler: sg.requestHandler,
|
|
610
|
-
successKind: "signup-requested",
|
|
611
|
-
appBaseUrl: sg.appActivationUrl,
|
|
612
|
-
sendEmail: ({ email, url, expiresAt }) =>
|
|
613
|
-
sg.sendActivationEmail({ email, activationUrl: url, expiresAt }), // @wrapper-known semantic-alias
|
|
614
568
|
});
|
|
615
569
|
|
|
616
570
|
api.post(Routes.authSignupConfirm, async (c) => {
|
|
@@ -955,37 +909,19 @@ export function createAuthRoutes(
|
|
|
955
909
|
}
|
|
956
910
|
|
|
957
911
|
// --- shared route builders for token flows ---------------------------------
|
|
958
|
-
// Password-reset
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
// the
|
|
964
|
-
//
|
|
965
|
-
// one edit, not two.
|
|
966
|
-
|
|
967
|
-
type TokenRequestData = {
|
|
968
|
-
kind: string;
|
|
969
|
-
email: string;
|
|
970
|
-
token: string;
|
|
971
|
-
expiresAt: string;
|
|
972
|
-
};
|
|
973
|
-
|
|
974
|
-
type TokenNoOp = { kind: "no-op" };
|
|
912
|
+
// Password-reset, email-verification and signup share the exact same request
|
|
913
|
+
// HTTP-shape: parse body → dispatch the request handler → always-200 (no
|
|
914
|
+
// enumeration). The handler mints the token AND dispatches the magic-link mail
|
|
915
|
+
// via delivery (ctx.notify); the route never sees the token. confirm-route
|
|
916
|
+
// validates the token + does the state change → typed failure or 200. Keeping
|
|
917
|
+
// the silent-success invariant in one place means changing how the framework
|
|
918
|
+
// handles "invalid_body" on a public token endpoint is one edit, not three.
|
|
975
919
|
|
|
976
920
|
function registerTokenRequestRoute(opts: {
|
|
977
921
|
api: Hono;
|
|
978
922
|
dispatcher: Dispatcher;
|
|
979
923
|
path: string;
|
|
980
924
|
requestHandler: string;
|
|
981
|
-
// Discriminator the feature handler emits when it actually minted a token
|
|
982
|
-
// (vs. the silent no-op for unknown/already-handled users).
|
|
983
|
-
successKind: string;
|
|
984
|
-
// Base URL of the receiving app page. `?token=…` is appended with proper
|
|
985
|
-
// separator handling so the caller's URL may or may not carry existing
|
|
986
|
-
// query params.
|
|
987
|
-
appBaseUrl: string;
|
|
988
|
-
sendEmail: (args: { email: string; url: string; expiresAt: string }) => Promise<void>;
|
|
989
925
|
}): void {
|
|
990
926
|
const body = RequestTokenBody;
|
|
991
927
|
opts.api.post(opts.path, async (c) => {
|
|
@@ -995,30 +931,11 @@ function registerTokenRequestRoute(opts: {
|
|
|
995
931
|
// anything from the shape of their input.
|
|
996
932
|
if (!parsed.success) return c.json({ isSuccess: true });
|
|
997
933
|
|
|
998
|
-
|
|
999
|
-
opts.requestHandler,
|
|
1000
|
-
{ email: parsed.data.email },
|
|
1001
|
-
GUEST_USER,
|
|
1002
|
-
);
|
|
1003
|
-
|
|
934
|
+
// The handler dispatches the magic-link mail via delivery before returning.
|
|
1004
935
|
// Handler-level failures (only legitimate reason: misconfiguration) are
|
|
1005
|
-
// silently swallowed — observability logs capture them for ops
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const data = result.data as TokenRequestData | TokenNoOp;
|
|
1009
|
-
if (data.kind === opts.successKind) {
|
|
1010
|
-
// TS narrowt nicht durch generic successKind (string, kein literal) —
|
|
1011
|
-
// die kind-Gleichheit garantiert den TokenRequestData-Branch hier.
|
|
1012
|
-
const requested = data as TokenRequestData; // @cast-boundary engine-payload
|
|
1013
|
-
const sep = opts.appBaseUrl.includes("?") ? "&" : "?";
|
|
1014
|
-
const url = `${opts.appBaseUrl}${sep}token=${encodeURIComponent(requested.token)}`;
|
|
1015
|
-
await opts.sendEmail({
|
|
1016
|
-
email: requested.email,
|
|
1017
|
-
url,
|
|
1018
|
-
expiresAt: requested.expiresAt,
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
936
|
+
// silently swallowed — observability logs capture them for ops — so the
|
|
937
|
+
// response shape stays uniform for unknown vs. known emails.
|
|
938
|
+
await opts.dispatcher.write(opts.requestHandler, { email: parsed.data.email }, GUEST_USER);
|
|
1022
939
|
|
|
1023
940
|
return c.json({ isSuccess: true });
|
|
1024
941
|
});
|