@cosmicdrift/kumiko-samples 0.384.0 → 0.385.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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
  4. package/packages/bundled-features/src/auth-email-password/password-policy.ts +196 -2087
  5. package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +9 -12
  6. package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
  7. package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
  8. package/packages/bundled-features/src/file-derivatives/feature.ts +1 -1
  9. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  10. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  11. package/packages/bundled-features/src/personal-access-tokens/changes.json +7 -0
  12. package/packages/bundled-features/src/personal-access-tokens/i18n.ts +4 -0
  13. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -0
  14. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +1 -1
  15. package/samples/apps/use-all-bundled/bin/main.ts +7 -1
  16. package/samples/apps/use-all-bundled/feature-manifest.json +15 -1
  17. package/samples/apps/use-all-bundled/kumiko/schema.ts +3 -1
  18. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -1
  19. package/samples/apps/use-all-bundled/src/app/pat-scope-labels-feature.ts +21 -0
  20. package/samples/apps/use-all-bundled/src/run-config.ts +5 -0
  21. package/samples/recipes/i18n/README.md +15 -0
  22. package/samples/recipes/i18n/src/feature.ts +4 -0
@@ -1,17 +1,14 @@
1
- // Shared Web-Primitives für die Auth-Screens. Nur noch Layout/Style-
2
- // Helpers — Form/Field/Input/Button/Banner kommen jetzt über
3
- // usePrimitives() aus dem Framework-Vertrag, damit Native dieselben
4
- // Auth-Screens rendern kann (renderer-native registriert eigene
5
- // Implementations).
1
+ // Shared web primitives for the auth screens. Only layout/style helpers are
2
+ // left here; Form/Field/Input/Button/Banner come through usePrimitives() from
3
+ // the framework contract, so apps and tests can override the implementations
4
+ // via PrimitivesProvider.
6
5
  //
7
- // <AuthCard> — Card-Wrapper für die Auth-Screen-Layouts
8
- // (full-screen, zentriert, max-w-sm). Web-only;
9
- // eine Native-Variante landet bei Bedarf
10
- // daneben (z.B. SafeArea + ScrollView).
11
- // parseUrlToken — URL-Param-Helper (window.location.search).
6
+ // <AuthCard> — card wrapper for the auth screen layouts
7
+ // (full-screen, centered, max-w-sm). Web-only.
8
+ // parseUrlToken — URL param helper (window.location.search).
12
9
  //
13
- // Link-Styles laufen über das Link-Primitive (variant="button"/"muted") —
14
- // die früheren authButtonClass/authMutedLinkClass sind dorthin gewandert.
10
+ // Link styles go through the Link primitive (variant="button"/"muted"); the
11
+ // former authButtonClass/authMutedLinkClass moved there.
15
12
 
16
13
  import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
17
14
  import { BareFormProvider, cn } from "@cosmicdrift/kumiko-renderer/web";
@@ -16,7 +16,7 @@ export const unreadCountQuery = defineQueryHandler({
16
16
  description:
17
17
  "Counts the calling user's unread in-app notification messages; use it for an inbox badge or to check whether a user has pending notifications.",
18
18
  handler: async (query, ctx) => {
19
- // bun-db hat keinen aggregate-helper — selectMany alle matching rows,
19
+ // The query helpers in db/query.ts have no aggregate helper — selectMany alle matching rows,
20
20
  // count() in JS. Pragma: unread-counts sind low-cardinality (user-
21
21
  // scoped, max ~hunderte rows). Wenn das wachsen sollte: raw .unsafe()
22
22
  // mit COUNT(*).
@@ -9,7 +9,7 @@ function quoteTable(tableName: string): string {
9
9
  function bindJsonbParam(value: unknown): { sql: string; bound: unknown } {
10
10
  // Scalar JSON primitives can't bind directly to ::jsonb — Postgres rejects a
11
11
  // bound boolean/number with "cannot cast type boolean/integer to jsonb" (and
12
- // Bun.SQL infers boolean[] candidates). Route them through ::text::jsonb with
12
+ // the driver may infer array candidates). Route them through ::text::jsonb with
13
13
  // a JSON-encoded literal. Objects/arrays/strings already bind as ::jsonb.
14
14
  if (typeof value === "boolean" || typeof value === "number") {
15
15
  return { sql: "$1::text::jsonb", bound: JSON.stringify(value) };
@@ -183,7 +183,7 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
183
183
  // answer identically, no name-oracle. fileRefsTable.id is a UUID
184
184
  // column — a malformed id must fail here, not at the DB. Without
185
185
  // this, an anonymous, internet-facing caller can throw a Postgres
186
- // 22P02 on every request (Bun.SQL pools the failed connection), a
186
+ // 22P02 on every request (the pooled connection is affected), a
187
187
  // DoS primitive with no auth required.
188
188
  if (!fileRefId || !variant || !VARIANT_NAME_RE.test(variant) || !isUuid(fileRefId)) {
189
189
  return c.text("not found", 404);
@@ -1,7 +1,7 @@
1
1
  // Client-safe ledger surface: QN constants, the pure recurring helpers
2
2
  // (projection + Soll/Ist merge), and the Zod payload schemas (for client-side
3
3
  // pre-validation before dispatch) — nothing else. The full `../ledger` entry
4
- // re-exports the feature/handlers/executor, which pull bun-db/postgres — a
4
+ // re-exports the feature/handlers/executor, which pull postgres-js — a
5
5
  // browser bundle that imports from there fails on Node builtins. A client screen
6
6
  // (e.g. a rent-cashflow view) imports the dispatch QNs + the pure forecast/merge
7
7
  // from HERE, then dispatches via the renderer's useDispatcher.
@@ -69,7 +69,7 @@ export function createAddNoteHandler(
69
69
  let authorName: string | null = null;
70
70
  try {
71
71
  // read_users is tenant-agnostic → ctx.db.unsafeRaw, not the tenant-scoped ctx.db.
72
- // Bun.SQL poisons the whole tx after any error inside it, even one that's
72
+ // postgres-js poisons the whole tx after any error inside it, even one that's
73
73
  // caught — so the lookup runs in a savepoint where available, otherwise directly
74
74
  // (pool statements are their own units).
75
75
  authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(), async (sp) => {
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.385.0",
4
+ "type": "improvement",
5
+ "title": "patScopeOptionKey for translating token scope labels",
6
+ "detail": "The scope labels personal-access-tokens generates from `scopes[domain].label` are English only. `patScopeOptionKey(domain, \"read\" | \"write\")` returns their translation key, so an app feature can add the other locales with `r.translations`.",
7
+ "migration": "Apps that mount personal-access-tokens and set `requiredLocales` with a locale other than `en` translate each generated scope label (`read`, plus `write` where the domain offers it) under `patScopeOptionKey`."
8
+ },
2
9
  {
3
10
  "version": "0.372.0",
4
11
  "type": "fix",
@@ -8,6 +8,10 @@ const mintFieldKey = (field: string): string =>
8
8
  const scopeOptionKey = (grant: string): string =>
9
9
  fieldOptionLabelKey(PAT_FEATURE, ACTION_FORM_ENTITY, "scopes", grant);
10
10
 
11
+ // The generated scope labels are English only; apps translate them under this key.
12
+ export const patScopeOptionKey = (domain: string, level: "read" | "write"): string =>
13
+ scopeOptionKey(`${domain}:${level}`);
14
+
11
15
  type LocalizedString = { readonly en: string };
12
16
 
13
17
  export const PAT_FEATURE_I18N: Readonly<Record<string, LocalizedString>> = {
@@ -8,6 +8,7 @@ export {
8
8
  } from "./feature.ts";
9
9
  export type { CreatePatOptions, PatMfaVerifyResult } from "./handlers/create.write.ts";
10
10
  export { hashPatToken, mintPatToken } from "./hash.ts";
11
+ export { patScopeOptionKey } from "./i18n.ts";
11
12
  export {
12
13
  PAT_REVOKED_AGGREGATE_TYPE,
13
14
  PAT_REVOKED_EVENT_QN,
@@ -1,7 +1,7 @@
1
1
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
2
 
3
3
  // Per-unit sub-transaction (one user, one purged row), nesting-aware across both db shapes:
4
- // - top-level connection (Bun.SQL / postgres-js Sql) → `.begin` (BEGIN)
4
+ // - top-level connection (postgres-js Sql) → `.begin` (BEGIN)
5
5
  // - TransactionSql (inside the dispatcher's outer tx, where every
6
6
  // writeHandler already runs) → `.savepoint` (SAVEPOINT)
7
7
  // A TransactionSql has no `.begin`, so the previous unconditional `.begin`
@@ -8,7 +8,12 @@ import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
8
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
10
10
  import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
11
- import { APP_FEATURES, AUTH_COMPOSE_OPTIONS, BOOT_FEATURES } from "../src/run-config";
11
+ import {
12
+ APP_FEATURES,
13
+ AUTH_COMPOSE_OPTIONS,
14
+ BOOT_FEATURES,
15
+ VALIDATE_BOOT_OPTIONS,
16
+ } from "../src/run-config";
12
17
 
13
18
  const SMOKE_TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
14
19
 
@@ -21,6 +26,7 @@ await runProdApp({
21
26
  features: BOOT_FEATURES,
22
27
  featureToggles: true,
23
28
  envSchema,
29
+ validateBootOptions: VALIDATE_BOOT_OPTIONS,
24
30
  // Smoke-only: exercises the hard PII boot gate + blind-index gate with
25
31
  // every bundled feature mounted. Real apps wire createPgKmsAdapter(...).
26
32
  kms: new InMemoryKmsAdapter(),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
3
- "featureCount": 86,
3
+ "featureCount": 87,
4
4
  "features": [
5
5
  {
6
6
  "name": "account-security",
@@ -2070,6 +2070,20 @@
2070
2070
  "secrets": [],
2071
2071
  "writeHandlers": []
2072
2072
  },
2073
+ {
2074
+ "name": "pat-scope-labels",
2075
+ "description": "German labels for the token scopes this sample app declares.",
2076
+ "toggleableDefault": null,
2077
+ "requires": [],
2078
+ "optionalRequires": [],
2079
+ "configReads": [],
2080
+ "exposesApis": [],
2081
+ "usesApis": [],
2082
+ "extensionsUsed": [],
2083
+ "configKeys": [],
2084
+ "secrets": [],
2085
+ "writeHandlers": []
2086
+ },
2073
2087
  {
2074
2088
  "name": "personal-access-tokens",
2075
2089
  "description": "Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
@@ -11,7 +11,9 @@
11
11
  import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
12
12
  import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
13
13
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
14
- import { APP_FEATURES, AUTH_COMPOSE_OPTIONS } from "../src/run-config";
14
+ import { APP_FEATURES, AUTH_COMPOSE_OPTIONS, VALIDATE_BOOT_OPTIONS } from "../src/run-config";
15
+
16
+ export { VALIDATE_BOOT_OPTIONS };
15
17
 
16
18
  export const FEATURES: readonly FeatureDefinition[] = composeFeatures(APP_FEATURES, {
17
19
  includeBundled: true,
@@ -13,7 +13,7 @@
13
13
  import { createTemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
14
14
  import type { RunDevAppOptions } from "@cosmicdrift/kumiko-dev-server";
15
15
  import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
16
- import { AUTH_COMPOSE_OPTIONS, BOOT_FEATURES } from "../run-config";
16
+ import { AUTH_COMPOSE_OPTIONS, BOOT_FEATURES, VALIDATE_BOOT_OPTIONS } from "../run-config";
17
17
  import { ADMIN_EMAIL, ADMIN_PASSWORD, BETA_TENANT_ID, DEV_TENANT_ID } from "./auth-constants";
18
18
  import { invoiceReportsFeature } from "./invoice-reports-feature";
19
19
  import { notesFeature } from "./notes-feature";
@@ -30,6 +30,7 @@ export const devAppOptions = {
30
30
  // it stays out of APP_FEATURES. appScreensFeature places the user-profile screen.
31
31
  features: [...BOOT_FEATURES, appScreensFeature, notesFeature, invoiceReportsFeature],
32
32
  featureToggles: true,
33
+ validateBootOptions: VALIDATE_BOOT_OPTIONS,
33
34
  port: Number.parseInt(process.env["PORT"] ?? "4186", 10),
34
35
  clientEntry: "./src/app/client.tsx",
35
36
  htmlPath: "./public/index.html",
@@ -0,0 +1,21 @@
1
+ import { patScopeOptionKey } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
2
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
+
4
+ // The scope domains in run-config.ts are the app's choice, so their German
5
+ // labels are too: personal-access-tokens only generates the English ones.
6
+ export const patScopeLabelsFeature = defineFeature("pat-scope-labels", (r) => {
7
+ r.describe("German labels for the token scopes this sample app declares.");
8
+ r.translations({
9
+ keys: {
10
+ [patScopeOptionKey("pages", "read")]: { de: "Seiten (lesen)" },
11
+ [patScopeOptionKey("pages", "write")]: { de: "Seiten (lesen & schreiben)" },
12
+ [patScopeOptionKey("tags", "read")]: { de: "Tags (lesen)" },
13
+ [patScopeOptionKey("tags", "write")]: { de: "Tags (lesen & schreiben)" },
14
+ [patScopeOptionKey("notesHistory", "read")]: { de: "Notizen (lesen)" },
15
+ [patScopeOptionKey("notesHistory", "write")]: { de: "Notizen (lesen & schreiben)" },
16
+ [patScopeOptionKey("ledger", "read")]: { de: "Hauptbuch (lesen)" },
17
+ [patScopeOptionKey("ledger", "write")]: { de: "Hauptbuch (lesen & schreiben)" },
18
+ },
19
+ });
20
+ return {};
21
+ });
@@ -102,6 +102,7 @@ import { DEV_TENANT_ID } from "./app/auth-constants";
102
102
  import { CAP_OVERVIEW_CAPS } from "./app/cap-overview-caps";
103
103
  import { capOverviewLabelsFeature } from "./app/cap-overview-labels-feature";
104
104
  import { collectionLabelsFeature } from "./app/collection-labels-feature";
105
+ import { patScopeLabelsFeature } from "./app/pat-scope-labels-feature";
105
106
 
106
107
  // Smoke-only stubs. Boot-mode skipt jede operative Methode — diese werden
107
108
  // nie aufgerufen, nur typecheck'd.
@@ -161,6 +162,7 @@ export const APP_FEATURES = [
161
162
  ledger: { label: "Ledger", read: ["ledger:query:*"], write: ["ledger:write:*"] },
162
163
  },
163
164
  }),
165
+ patScopeLabelsFeature,
164
166
  createReadinessFeature(),
165
167
 
166
168
  // delivery + channels
@@ -417,6 +419,9 @@ export const BOOT_FEATURES = APP_FEATURES.filter(
417
419
  // Smoke signup — enables createAuthSelfRegistrationToggleFeature via
418
420
  // composeFeatures when passed as authOptions (#1521 Option A). Same shape
419
421
  // as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
422
+ // locale-de is mounted, so every translation key must exist in German and English.
423
+ export const VALIDATE_BOOT_OPTIONS = { requiredLocales: ["de", "en"] } as const;
424
+
420
425
  export const AUTH_COMPOSE_OPTIONS = {
421
426
  signup: {
422
427
  appUrl: "http://localhost:4186/signup",
@@ -6,6 +6,7 @@ Feature-scoped translations — 'Hallo' and 'Hello' in the same app.
6
6
 
7
7
  - `r.translations()` for multi-language feature keys
8
8
  - Plurals
9
+ - `requiredLocales`: boot fails when a key misses one of the app's languages
9
10
 
10
11
  ## Plurals
11
12
 
@@ -24,6 +25,20 @@ exist where the locale's grammar needs them:
24
25
  `Intl.PluralRules(locale)`, then interpolates `{count}` (and any other
25
26
  `{name}` placeholder) into the chosen form.
26
27
 
28
+ ## Required locales
29
+
30
+ Without further options boot only checks that each feature is consistent with
31
+ itself. An app that ships fixed languages lists them, and boot then fails on any
32
+ key that misses one of them, including framework keys. Locale packages such as
33
+ `locale-de` count:
34
+
35
+ ```ts illustration
36
+ runProdApp({ ...options, validateBootOptions: { requiredLocales: ["de", "en"] } });
37
+ ```
38
+
39
+ Define the options once and re-export them as `VALIDATE_BOOT_OPTIONS` from
40
+ `kumiko/schema.ts`, so `kumiko-schema validate` checks the same list in CI.
41
+
27
42
  ## Source
28
43
 
29
44
  Feature entry point: `src/feature.ts`.
@@ -11,20 +11,24 @@ export const greetingFeature = defineFeature("greeting", (r) => {
11
11
  de: "Willkommen",
12
12
  en: "Welcome",
13
13
  fr: "Bienvenue",
14
+ pl: "Witamy",
14
15
  },
15
16
  "greeting.goodbye": {
16
17
  de: "Auf Wiedersehen",
17
18
  en: "Goodbye",
18
19
  fr: "Au revoir",
20
+ pl: "Do widzenia",
19
21
  },
20
22
  "greeting.hello_name": {
21
23
  de: "Hallo, {name}!",
22
24
  en: "Hello, {name}!",
23
25
  fr: "Bonjour, {name}!",
26
+ pl: "Cześć, {name}!",
24
27
  },
25
28
  "greeting.unread_count": {
26
29
  de: { one: "{count} ungelesene Nachricht", other: "{count} ungelesene Nachrichten" },
27
30
  en: { one: "{count} unread message", other: "{count} unread messages" },
31
+ fr: { one: "{count} message non lu", other: "{count} messages non lus" },
28
32
  pl: {
29
33
  one: "{count} nieprzeczytana wiadomość",
30
34
  few: "{count} nieprzeczytane wiadomości",