@cosmicdrift/kumiko-framework 0.102.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.102.1",
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.102.1",
184
+ "@cosmicdrift/kumiko-dispatcher-live": "0.104.0",
185
185
  "bun-types": "^1.3.13",
186
186
  "pino-pretty": "^13.1.3"
187
187
  },
@@ -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. Confirm
356
- // returnt `{ kind: "auth-session", session, tenantKey }` analog zu
357
- // loginHandler, sodass die Route JWT minten + Cookies setzen kann
358
- // (Auto-Login direkt nach Activation, kein zweiter login-Roundtrip).
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 and email-verification share the exact same HTTP-shape:
959
- // request-route emits a token optional sendEmail callbacksilent-success,
960
- // confirm-route validates token + does the state change typed failure or
961
- // 200. Before this extraction both flows carried ~45 LOC of nearly-identical
962
- // body-parse / dispatch / url-build / response plumbing. The helpers keep
963
- // the public-facing silent-success invariant in one place changing how
964
- // the framework handles "invalid_body" on a public token endpoint is now
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 bodydispatch 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
- const result = await opts.dispatcher.write(
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
- if (result.isSuccess) {
1007
- // @cast-boundary engine-payload — generic dispatcher.write result narrowed by handler-emitted kind
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
  });