@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/password-policy.ts +196 -2087
- package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +9 -12
- package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/feature.ts +1 -1
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/changes.json +7 -0
- package/packages/bundled-features/src/personal-access-tokens/i18n.ts +4 -0
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -0
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +1 -1
- package/samples/apps/use-all-bundled/bin/main.ts +7 -1
- package/samples/apps/use-all-bundled/feature-manifest.json +15 -1
- package/samples/apps/use-all-bundled/kumiko/schema.ts +3 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/pat-scope-labels-feature.ts +21 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +5 -0
- package/samples/recipes/i18n/README.md +15 -0
- package/samples/recipes/i18n/src/feature.ts +4 -0
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
// Shared
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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> —
|
|
8
|
-
// (full-screen,
|
|
9
|
-
//
|
|
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
|
|
14
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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 {
|
|
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":
|
|
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",
|