@cosmicdrift/kumiko-framework 0.334.0 → 0.335.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/dist/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +5 -2
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +103 -33
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +62 -19
- package/dist/engine/build-config-feature-schema.js +8 -1
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/index.d.ts +4 -3
- package/dist/engine/index.js +3 -2
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +1 -0
- package/dist/engine/schema-builder.js +9 -2
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +23 -35
- package/dist/i18n/settings-hub-keys.js +3 -0
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +22 -5
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +206 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
|
@@ -3,7 +3,7 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
3
3
|
import { getUser } from "../api/auth-middleware.js";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
5
|
import { selectMany } from "../bun-db/index.js";
|
|
6
|
-
import { collectPiiSubjectFields, configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, } from "../crypto/index.js";
|
|
6
|
+
import { collectPiiSubjectFields, configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, PII_ERASED_SENTINEL, } from "../crypto/index.js";
|
|
7
7
|
import { createEventStoreExecutor } from "../db/event-store-executor.js";
|
|
8
8
|
import { createTenantDb } from "../db/tenant-db.js";
|
|
9
9
|
import { createDerivativesContext, resolveFieldVariant, resolveRenderer, } from "../derivatives/index.js";
|
|
@@ -106,11 +106,27 @@ export function createFileRoutes(options) {
|
|
|
106
106
|
if (!isPiiCiphertext(fileRef.fileName))
|
|
107
107
|
return fileRef.fileName;
|
|
108
108
|
const kms = configuredPiiSubjectKms();
|
|
109
|
+
// skip: the framework has no logger at this route layer and the file bytes are
|
|
110
|
+
// still served; the "download" fallback only hides the stored ciphertext name.
|
|
109
111
|
if (!kms) {
|
|
110
112
|
return "download";
|
|
111
113
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
try {
|
|
115
|
+
const decrypted = await decryptPiiFieldValues({ fileName: fileRef.fileName }, piiSubjectFields, kms, kmsContextFor());
|
|
116
|
+
const name = decrypted["fileName"];
|
|
117
|
+
// decryptPiiFieldValues returns the row untouched when fileName isn't
|
|
118
|
+
// annotated as PII (annotation drift), so ciphertext/erased sentinel
|
|
119
|
+
// can still be here and must never reach a header.
|
|
120
|
+
if (typeof name !== "string" || isPiiCiphertext(name) || name === PII_ERASED_SENTINEL) {
|
|
121
|
+
return "download";
|
|
122
|
+
}
|
|
123
|
+
return name;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// skip: broken crypto metadata (malformed ciphertext, auth failure, missing key)
|
|
127
|
+
// must not make intact bytes unreachable; the route layer has no logger.
|
|
128
|
+
return "download";
|
|
129
|
+
}
|
|
114
130
|
}
|
|
115
131
|
// Runs before parseBody buffers the request below: checks Content-Length
|
|
116
132
|
// upfront, counts chunked bodies while streaming.
|
|
@@ -397,11 +413,10 @@ export function createFileRoutes(options) {
|
|
|
397
413
|
if (!row)
|
|
398
414
|
return c.json({ error: "not_found" }, 404);
|
|
399
415
|
const fileRef = row; // @cast-boundary db-row (decrypted via executor.detail)
|
|
400
|
-
// detail()'s "pass"-ownership read widens to tenantId IN (self, SYSTEM)
|
|
401
|
-
// and doesn't filter isDeleted (list() does, detail() doesn't) — both
|
|
416
|
+
// detail()'s "pass"-ownership read widens to tenantId IN (self, SYSTEM),
|
|
402
417
|
// narrower in loadFileForTenant's selectMany. Restore that here so meta
|
|
403
|
-
// doesn't leak reference-tenant
|
|
404
|
-
if (fileRef.tenantId !== user.tenantId
|
|
418
|
+
// doesn't leak reference-tenant rows.
|
|
419
|
+
if (fileRef.tenantId !== user.tenantId) {
|
|
405
420
|
return c.json({ error: "not_found" }, 404);
|
|
406
421
|
}
|
|
407
422
|
const decision = await guard({ fileRef, user, operation: "read" });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
5
|
import { assertSafeStorageKey } from "./types.js";
|
|
6
6
|
// Local-filesystem backend — intended for dev + tests. Production deploys
|
|
@@ -101,25 +101,28 @@ export function createLocalProvider(basePath) {
|
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
async list(prefix) {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
104
|
+
// Walk only the directory part of the prefix: a tenant-scoped sweep must
|
|
105
|
+
// not traverse every other tenant's files in a shared storage root.
|
|
106
|
+
// Keys are normalized to "/" (storage keys are always "/"-joined,
|
|
107
|
+
// matching S3) before the final prefix match.
|
|
108
|
+
const prefixDir = prefix.slice(0, prefix.lastIndexOf("/") + 1);
|
|
109
|
+
const walkRoot = prefixDir === "" ? resolvedBase : resolveContainedPath(prefixDir);
|
|
108
110
|
let entries;
|
|
109
111
|
try {
|
|
110
|
-
entries = await readdir(
|
|
112
|
+
entries = await readdir(walkRoot, { recursive: true, withFileTypes: true });
|
|
111
113
|
}
|
|
112
114
|
catch (err) {
|
|
113
|
-
|
|
115
|
+
const code = err.code;
|
|
116
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
114
117
|
return [];
|
|
115
118
|
throw err;
|
|
116
119
|
}
|
|
117
120
|
const results = [];
|
|
118
121
|
for (const entry of entries) {
|
|
119
|
-
|
|
120
|
-
if (!key.startsWith(prefix))
|
|
122
|
+
if (!entry.isFile())
|
|
121
123
|
continue;
|
|
122
|
-
|
|
124
|
+
const key = relative(resolvedBase, join(entry.parentPath, entry.name)).split(sep).join("/");
|
|
125
|
+
if (key.startsWith(prefix))
|
|
123
126
|
results.push(key);
|
|
124
127
|
}
|
|
125
128
|
return results;
|
|
@@ -50,8 +50,27 @@ export async function createFileProviderForTenant(ctx, tenantId, handlerName = "
|
|
|
50
50
|
throw new Error(`${FEATURE_NAME}: provider "${provider}" registered without a build() — ` +
|
|
51
51
|
`extension options must be a FileProviderPlugin.`);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
const built = await usage.options.build(ctx, tenantId);
|
|
54
|
+
// Extension payloads are never type-checked, so a plugin predating a
|
|
55
|
+
// required method (list) would otherwise surface as a misleading
|
|
56
|
+
// "missing s3:ListBucket permission" hint during erasure.
|
|
57
|
+
for (const method of REQUIRED_PROVIDER_METHODS) {
|
|
58
|
+
if (typeof built[method] !== "function") {
|
|
59
|
+
throw new Error(`${FEATURE_NAME}: provider "${provider}" built without the required method ${method}() — ` +
|
|
60
|
+
`update the file-provider plugin to the current FileStorageProvider contract.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return built;
|
|
54
64
|
}
|
|
65
|
+
const REQUIRED_PROVIDER_METHODS = [
|
|
66
|
+
"write",
|
|
67
|
+
"writeStream",
|
|
68
|
+
"read",
|
|
69
|
+
"readStream",
|
|
70
|
+
"delete",
|
|
71
|
+
"exists",
|
|
72
|
+
"list",
|
|
73
|
+
];
|
|
55
74
|
// Builds the resolver from the ambient AppContext fields — the framework-side
|
|
56
75
|
// equivalent of the GDPR `makeTenantStorageProviderResolver`. The config
|
|
57
76
|
// accessor (and therefore the s3.secretAccessKey secret-read) runs under
|
package/dist/files/types.js
CHANGED
|
@@ -207,6 +207,15 @@ export function validateFileContent(fileName, content, accept) {
|
|
|
207
207
|
const ext = extractExtension(fileName);
|
|
208
208
|
if (!ext)
|
|
209
209
|
return { kind: "ok" };
|
|
210
|
+
// validateFile accepts aliased declared MIMEs (csv as vnd.ms-excel) on
|
|
211
|
+
// metadata alone, so the byte check has to live on this exported path too:
|
|
212
|
+
// a text-format extension with an alias must never carry a binary signature.
|
|
213
|
+
if (Object.hasOwn(EXTENSION_MIME_ALIASES, ext) && sniffMimeType(content) !== null) {
|
|
214
|
+
return {
|
|
215
|
+
kind: "rejected",
|
|
216
|
+
error: `content_mismatch: ".${ext}" upload bytes are a binary format`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
210
219
|
const signatureMimeTypes = signatureMimeTypesForExtension(ext);
|
|
211
220
|
if (signatureMimeTypes.length === 0)
|
|
212
221
|
return { kind: "ok" };
|
|
@@ -261,9 +270,15 @@ extensionOverride) {
|
|
|
261
270
|
const ext = /^[A-Za-z0-9]+$/.test(rawExt) ? rawExt.toLowerCase() : "bin";
|
|
262
271
|
return `${tenantId}/${entityType}/${entityId}/${fieldName}/${uniqueId}.${ext}`;
|
|
263
272
|
}
|
|
273
|
+
const EXPORTS_PREFIX_SEGMENT = "exports";
|
|
264
274
|
/** Fixed leading segment so one S3 lifecycle rule (Prefix: "exports/") can expire export bundles for every tenant without matching a buildStorageKey() upload. */
|
|
265
275
|
export function tenantExportPrefix(tenantId) {
|
|
266
|
-
|
|
276
|
+
// A tenant named like the reserved segment would make the sweep prefix
|
|
277
|
+
// "exports/" match every tenant's export bundles.
|
|
278
|
+
if (tenantId === EXPORTS_PREFIX_SEGMENT) {
|
|
279
|
+
throw new Error(`tenantId "${tenantId}" collides with the reserved storage segment`);
|
|
280
|
+
}
|
|
281
|
+
return `${EXPORTS_PREFIX_SEGMENT}/${tenantId}/`;
|
|
267
282
|
}
|
|
268
283
|
/**
|
|
269
284
|
* Every storage-key prefix a tenant's binaries can live under. A tenant-destroy
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export declare const DEFAULT_LOCALE = "en";
|
|
2
2
|
export declare function isValidLocaleTag(value: string): boolean;
|
|
3
|
-
/** BCP-47 is case-insensitive; canonicalize the
|
|
4
|
-
* so registry lookups (`mailT`, `hasMailTranslations`)
|
|
3
|
+
/** BCP-47 is case-insensitive; canonicalize to the RFC 5646 conventional form
|
|
4
|
+
* (`de-AT`, `zh-Hant-TW`) so registry lookups (`mailT`, `hasMailTranslations`)
|
|
5
|
+
* hit registered keys regardless of header casing. */
|
|
5
6
|
export declare function canonicalizeLocaleTag(tag: string): string;
|
|
6
7
|
/**
|
|
7
8
|
* Picks the best well-formed tag from an Accept-Language header (RFC 9110
|
|
@@ -14,13 +14,23 @@ const MAX_LOCALE_TAG_LENGTH = 35;
|
|
|
14
14
|
export function isValidLocaleTag(value) {
|
|
15
15
|
return value.length <= MAX_LOCALE_TAG_LENGTH && LOCALE_TAG_RE.test(value);
|
|
16
16
|
}
|
|
17
|
-
/** BCP-47 is case-insensitive; canonicalize the
|
|
18
|
-
* so registry lookups (`mailT`, `hasMailTranslations`)
|
|
17
|
+
/** BCP-47 is case-insensitive; canonicalize to the RFC 5646 conventional form
|
|
18
|
+
* (`de-AT`, `zh-Hant-TW`) so registry lookups (`mailT`, `hasMailTranslations`)
|
|
19
|
+
* hit registered keys regardless of header casing. */
|
|
19
20
|
export function canonicalizeLocaleTag(tag) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
return tag
|
|
22
|
+
.split("-")
|
|
23
|
+
.map((subtag, index) => {
|
|
24
|
+
if (index === 0)
|
|
25
|
+
return subtag.toLowerCase();
|
|
26
|
+
if (/^[A-Za-z]{2}$/.test(subtag))
|
|
27
|
+
return subtag.toUpperCase();
|
|
28
|
+
if (/^[A-Za-z]{4}$/.test(subtag)) {
|
|
29
|
+
return subtag.charAt(0).toUpperCase() + subtag.slice(1).toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
return subtag.toLowerCase();
|
|
32
|
+
})
|
|
33
|
+
.join("-");
|
|
24
34
|
}
|
|
25
35
|
/**
|
|
26
36
|
* Picks the best well-formed tag from an Accept-Language header (RFC 9110
|
|
@@ -46,6 +46,24 @@ function pushSectionTitles(out, section, treatAsKey = false) {
|
|
|
46
46
|
for (const group of section.groups ?? [])
|
|
47
47
|
pushKey(out, group.title, treatAsKey);
|
|
48
48
|
}
|
|
49
|
+
// Field labels and section titles of a step rendered through the action-form shim.
|
|
50
|
+
function pushActionFormShapedKeys(out, featureName, step) {
|
|
51
|
+
for (const fieldName of Object.keys(step.fields)) {
|
|
52
|
+
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
53
|
+
}
|
|
54
|
+
for (const section of step.layout.sections) {
|
|
55
|
+
if (isExtensionEditSection(section)) {
|
|
56
|
+
pushKey(out, section.title);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (section.kind === "relatedList")
|
|
60
|
+
continue; // rejected at boot, unreachable here
|
|
61
|
+
pushSectionTitles(out, section);
|
|
62
|
+
for (const f of sectionFieldSpecs(section)) {
|
|
63
|
+
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, editFieldName(f)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
function pushRowActionKeys(out, action) {
|
|
50
68
|
pushKey(out, action.label);
|
|
51
69
|
if (action.kind === "writeHandler" || action.kind === undefined) {
|
|
@@ -242,26 +260,9 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
242
260
|
break;
|
|
243
261
|
}
|
|
244
262
|
case "secretMint": {
|
|
245
|
-
// Mirrors the actionForm case above (submitLabel, fields/section titles
|
|
246
|
-
// via ACTION_FORM_ENTITY) plus the reveal-phase's own i18n surface.
|
|
247
263
|
const mint = screen;
|
|
248
264
|
pushKey(out, mint.submitLabel);
|
|
249
|
-
|
|
250
|
-
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
251
|
-
}
|
|
252
|
-
for (const section of mint.layout.sections) {
|
|
253
|
-
if (isExtensionEditSection(section)) {
|
|
254
|
-
pushKey(out, section.title);
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (section.kind === "relatedList")
|
|
258
|
-
continue; // rejected at boot, unreachable here
|
|
259
|
-
pushSectionTitles(out, section);
|
|
260
|
-
for (const f of sectionFieldSpecs(section)) {
|
|
261
|
-
const fieldName = editFieldName(f);
|
|
262
|
-
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
+
pushActionFormShapedKeys(out, featureName, mint);
|
|
265
266
|
pushKey(out, mint.reveal.title);
|
|
266
267
|
pushKey(out, mint.reveal.warning);
|
|
267
268
|
pushKey(out, mint.reveal.confirmLabel);
|
|
@@ -271,22 +272,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
271
272
|
if (mint.confirm !== undefined) {
|
|
272
273
|
pushKey(out, mint.confirm.submitLabel);
|
|
273
274
|
pushKey(out, mint.confirm.doneMessage);
|
|
274
|
-
|
|
275
|
-
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
276
|
-
}
|
|
277
|
-
for (const section of mint.confirm.layout.sections) {
|
|
278
|
-
if (isExtensionEditSection(section)) {
|
|
279
|
-
pushKey(out, section.title);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (section.kind === "relatedList")
|
|
283
|
-
continue; // rejected at boot, unreachable here
|
|
284
|
-
pushSectionTitles(out, section);
|
|
285
|
-
for (const f of sectionFieldSpecs(section)) {
|
|
286
|
-
const fieldName = editFieldName(f);
|
|
287
|
-
out.add(fieldLabelKey(featureName, ACTION_FORM_ENTITY, fieldName));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
275
|
+
pushActionFormShapedKeys(out, featureName, mint.confirm);
|
|
290
276
|
}
|
|
291
277
|
break;
|
|
292
278
|
}
|
|
@@ -353,6 +339,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
353
339
|
}
|
|
354
340
|
if (isWriteFormEditSection(section)) {
|
|
355
341
|
pushKey(out, section.title);
|
|
342
|
+
pushKey(out, section.description);
|
|
356
343
|
pushKey(out, section.submitLabel);
|
|
357
344
|
// kumiko-lint-ignore section-fields-raw writeForm sections carry no groups (EditWriteFormSection)
|
|
358
345
|
for (const f of section.fields) {
|
|
@@ -404,7 +391,8 @@ function collectEntityListFilterKeys(feature, out) {
|
|
|
404
391
|
out.add(key);
|
|
405
392
|
}
|
|
406
393
|
}
|
|
407
|
-
else if (def.type === "select"
|
|
394
|
+
else if ((def.type === "select" || def.type === "multiSelect") &&
|
|
395
|
+
Array.isArray(def.options)) {
|
|
408
396
|
for (const value of def.options) {
|
|
409
397
|
out.add(selectFacetOptionKey(feature.name, screen.entity, fieldName, value));
|
|
410
398
|
}
|
|
@@ -39,5 +39,8 @@ export const SETTINGS_HUB_I18N = {
|
|
|
39
39
|
"config.secrets.placeholder": { en: "Enter a value" },
|
|
40
40
|
"config.secrets.replacePlaceholder": { en: "Enter a new value to replace it" },
|
|
41
41
|
"config.secrets.delete": { en: "Remove" },
|
|
42
|
+
"config.secrets.deleteConfirm": {
|
|
43
|
+
en: "Remove this secret? Anything using it stops working, and it cannot be restored.",
|
|
44
|
+
},
|
|
42
45
|
"screen:secrets.title": { en: "Secrets" },
|
|
43
46
|
};
|
package/dist/jobs/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { JobLogEntry, JobMeta, JobOutcomeMeta, JobRunner, JobRunnerOptions, } from "./job-runner.js";
|
|
2
|
-
export { createJobRunner } from "./job-runner.js";
|
|
1
|
+
export type { JobLogEntry, JobMeta, JobOutcomeMeta, JobRunner, JobRunnerOptions, JobSubjectValue, } from "./job-runner.js";
|
|
2
|
+
export { createJobRunner, serializeJobSubject } from "./job-runner.js";
|
package/dist/jobs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createJobRunner } from "./job-runner.js";
|
|
1
|
+
export { createJobRunner, serializeJobSubject } from "./job-runner.js";
|
|
@@ -28,6 +28,8 @@ export type JobOutcomeMeta = {
|
|
|
28
28
|
readonly messageKey: string | null;
|
|
29
29
|
} | undefined;
|
|
30
30
|
};
|
|
31
|
+
export type JobSubjectValue = string | number | boolean | null;
|
|
32
|
+
export declare function serializeJobSubject(subject: Readonly<Record<string, JobSubjectValue>>): string;
|
|
31
33
|
export type JobRunner = {
|
|
32
34
|
start(): Promise<void>;
|
|
33
35
|
stop(): Promise<void>;
|