@jini-ai/cms 0.3.10 → 0.4.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/CHANGELOG.md +59 -0
- package/README.md +47 -19
- package/dist/content-types/agent-tools.d.ts +3 -56
- package/dist/content-types/agent-tools.js +22 -6
- package/dist/content-types/cleanup.d.ts +18 -13
- package/dist/content-types/cleanup.js +16 -11
- package/dist/content-types/errors.d.ts +57 -24
- package/dist/content-types/errors.js +56 -39
- package/dist/content-types/field-defs.d.ts +8 -4
- package/dist/content-types/field-defs.js +19 -14
- package/dist/content-types/index-provisioning.d.ts +61 -47
- package/dist/content-types/index-provisioning.js +90 -66
- package/dist/content-types/index.d.ts +4 -3
- package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
- package/dist/content-types/lifecycle-dispatch.js +5 -4
- package/dist/content-types/lifecycle.d.ts +23 -18
- package/dist/content-types/lifecycle.js +71 -64
- package/dist/content-types/repo.memory.d.ts +5 -4
- package/dist/content-types/repo.memory.js +3 -2
- package/dist/content-types/tool-registrations.d.ts +6 -8
- package/dist/content-types/tool-registrations.js +24 -28
- package/dist/content-types/types.d.ts +17 -13
- package/dist/content-types/types.js +11 -5
- package/dist/content-types/write-service.d.ts +20 -22
- package/dist/content-types/write-service.js +78 -75
- package/dist/core/authorization.d.ts +15 -0
- package/dist/core/authorization.js +2 -0
- package/dist/core/commands/change-set.d.ts +15 -7
- package/dist/core/commands/command.d.ts +45 -26
- package/dist/core/commands/command.js +73 -63
- package/dist/core/entity-liveness.d.ts +11 -7
- package/dist/core/entity-liveness.js +5 -4
- package/dist/core/index.d.ts +12 -5
- package/dist/core/index.js +10 -4
- package/dist/core/ports.d.ts +25 -24
- package/dist/core/ports.js +1 -0
- package/dist/core/tools/args.d.ts +49 -0
- package/dist/core/tools/args.js +46 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/entries/agent-tools.d.ts +1 -73
- package/dist/entries/agent-tools.js +19 -13
- package/dist/entries/errors.d.ts +28 -14
- package/dist/entries/errors.js +17 -11
- package/dist/entries/field-validation.d.ts +5 -3
- package/dist/entries/field-validation.js +7 -5
- package/dist/entries/index.d.ts +2 -2
- package/dist/entries/lifecycle-dispatch.d.ts +5 -2
- package/dist/entries/lifecycle-dispatch.js +2 -1
- package/dist/entries/list.d.ts +4 -2
- package/dist/entries/list.js +4 -3
- package/dist/entries/repo.memory.d.ts +12 -10
- package/dist/entries/repo.memory.js +11 -10
- package/dist/entries/tool-registrations.d.ts +6 -8
- package/dist/entries/tool-registrations.js +42 -41
- package/dist/entries/types.d.ts +0 -7
- package/dist/entries/write-service.d.ts +28 -29
- package/dist/entries/write-service.js +114 -105
- package/dist/http/settings/cms-adapter.d.ts +20 -0
- package/dist/http/settings/cms-adapter.js +124 -0
- package/dist/http/settings/contracts.d.ts +135 -0
- package/dist/http/settings/contracts.js +2 -0
- package/dist/http/settings/http.d.ts +10 -0
- package/dist/http/settings/http.js +211 -0
- package/dist/http/settings/index.d.ts +5 -0
- package/dist/http/settings/index.js +4 -0
- package/dist/http/settings/shared.d.ts +68 -0
- package/dist/http/settings/shared.js +54 -0
- package/dist/http/settings/stream.d.ts +24 -0
- package/dist/http/settings/stream.js +147 -0
- package/dist/media/agent-tools.d.ts +1 -66
- package/dist/media/agent-tools.js +5 -1
- package/dist/media/blob-gc-lock.d.ts +6 -2
- package/dist/media/blob-gc-lock.js +4 -2
- package/dist/media/blob-gc.d.ts +64 -51
- package/dist/media/blob-gc.js +62 -52
- package/dist/media/blob-key.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts +3 -1
- package/dist/media/content-type-sniffer.js +1 -1
- package/dist/media/html-attributes.d.ts +9 -5
- package/dist/media/html-attributes.js +8 -6
- package/dist/media/image-transformer.js +1 -1
- package/dist/media/image-transformer.sharp.d.ts +6 -0
- package/dist/media/image-transformer.sharp.js +12 -6
- package/dist/media/import/fetch-image.d.ts +24 -0
- package/dist/media/import/fetch-image.js +137 -0
- package/dist/media/import/index.d.ts +3 -0
- package/dist/media/import/index.js +3 -0
- package/dist/media/import/ports.d.ts +38 -0
- package/dist/media/index.d.ts +7 -1
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +14 -10
- package/dist/media/media-service.js +57 -52
- package/dist/media/ports.d.ts +3 -2
- package/dist/media/rendition-service.d.ts +3 -3
- package/dist/media/rendition-service.js +38 -37
- package/dist/media/repo.memory.d.ts +18 -7
- package/dist/media/repo.memory.js +12 -6
- package/dist/media/tool-registrations.d.ts +16 -15
- package/dist/media/tool-registrations.js +84 -72
- package/dist/media/transform-lock.d.ts +22 -12
- package/dist/media/transform-lock.js +14 -13
- package/dist/media/transform-registry.d.ts +3 -3
- package/dist/media/transform-registry.js +19 -18
- package/dist/media/transform-types.d.ts +8 -3
- package/dist/media/transform-types.js +10 -5
- package/dist/media/types.d.ts +21 -4
- package/dist/media/types.js +15 -2
- package/dist/navigation/agent-tools.d.ts +1 -80
- package/dist/navigation/agent-tools.js +15 -3
- package/dist/navigation/contracts.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/menu-service.d.ts +57 -40
- package/dist/navigation/menu-service.js +80 -69
- package/dist/navigation/ports.d.ts +1 -1
- package/dist/navigation/read-model.d.ts +3 -1
- package/dist/navigation/read-model.js +2 -2
- package/dist/navigation/reconcile.d.ts +2 -2
- package/dist/navigation/reconcile.js +2 -1
- package/dist/navigation/repo.memory.d.ts +7 -3
- package/dist/navigation/repo.memory.js +4 -2
- package/dist/navigation/resolver.d.ts +4 -1
- package/dist/navigation/resolver.js +1 -1
- package/dist/navigation/tool-registrations.d.ts +6 -8
- package/dist/navigation/tool-registrations.js +44 -42
- package/dist/navigation/types.d.ts +1 -1
- package/dist/presentation/presentation.d.ts +11 -3
- package/dist/presentation/presentation.js +13 -6
- package/dist/presentation/repo.memory.d.ts +6 -2
- package/dist/presentation/repo.memory.js +3 -2
- package/dist/settings/agent-tools.d.ts +2 -75
- package/dist/settings/agent-tools.js +49 -45
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +154 -0
- package/dist/settings/agent-writable-preferences.js +8 -20
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +11 -2
- package/dist/settings/change-feed.js +4 -4
- package/dist/settings/definitions-dispatch.d.ts +8 -3
- package/dist/settings/definitions-dispatch.js +5 -5
- package/dist/settings/dictionaries/index.d.ts +5 -2
- package/dist/settings/dictionaries/index.js +2 -2
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
- package/dist/settings/ensure-definitions.d.ts +5 -5
- package/dist/settings/errors.d.ts +7 -1
- package/dist/settings/errors.js +7 -1
- package/dist/settings/index.d.ts +4 -1
- package/dist/settings/index.js +1 -0
- package/dist/settings/ports.d.ts +6 -5
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +5 -4
- package/dist/settings/purge-service.js +3 -2
- package/dist/settings/settings.d.ts +22 -7
- package/dist/settings/settings.js +47 -26
- package/dist/settings/tool-registrations.d.ts +25 -32
- package/dist/settings/tool-registrations.js +66 -56
- package/dist/settings/types.d.ts +1 -1
- package/dist/settings/ui-tab-definitions.d.ts +1 -1
- package/dist/settings/write-service.d.ts +32 -26
- package/dist/settings/write-service.js +26 -20
- package/dist/taxonomy/agent-tools.d.ts +1 -66
- package/dist/taxonomy/agent-tools.js +25 -15
- package/dist/taxonomy/content-lookup.js +1 -1
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/merge-term.d.ts +7 -4
- package/dist/taxonomy/merge-term.js +7 -5
- package/dist/taxonomy/repo.memory.d.ts +42 -19
- package/dist/taxonomy/repo.memory.js +14 -12
- package/dist/taxonomy/validation-chain.d.ts +39 -18
- package/dist/taxonomy/validation-chain.js +35 -25
- package/dist/taxonomy/write-service.d.ts +106 -45
- package/dist/taxonomy/write-service.js +221 -179
- package/dist/trash/adapters/record-store.d.ts +52 -0
- package/dist/trash/adapters/record-store.js +36 -0
- package/dist/trash/cursor.d.ts +20 -0
- package/dist/trash/cursor.js +36 -0
- package/dist/trash/follow-ups.d.ts +76 -0
- package/dist/trash/follow-ups.js +46 -0
- package/dist/trash/index.d.ts +8 -0
- package/dist/trash/index.js +8 -0
- package/dist/trash/ports.d.ts +308 -0
- package/dist/trash/ports.js +2 -0
- package/dist/trash/repo.memory.d.ts +50 -0
- package/dist/trash/repo.memory.js +114 -0
- package/dist/trash/sweeper.d.ts +77 -0
- package/dist/trash/sweeper.js +127 -0
- package/dist/trash/write-service.d.ts +100 -0
- package/dist/trash/write-service.js +242 -0
- package/dist/workspace/agent-tools.d.ts +1 -59
- package/dist/workspace/agent-tools.js +27 -12
- package/dist/workspace/create.d.ts +31 -6
- package/dist/workspace/create.js +18 -5
- package/dist/workspace/delete.d.ts +18 -13
- package/dist/workspace/delete.js +20 -12
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/repo.memory.d.ts +19 -4
- package/dist/workspace/repo.memory.js +26 -4
- package/dist/workspace/tool-registrations.d.ts +2 -2
- package/dist/workspace/tool-registrations.js +13 -13
- package/dist/workspace/update.d.ts +6 -5
- package/dist/workspace/update.js +9 -8
- package/package.json +44 -38
- package/dist/identity/admin-crud-service.d.ts +0 -280
- package/dist/identity/admin-crud-service.js +0 -468
- package/dist/identity/agent-tool-input.d.ts +0 -58
- package/dist/identity/agent-tool-input.js +0 -126
- package/dist/identity/agent-tools.d.ts +0 -101
- package/dist/identity/agent-tools.js +0 -290
- package/dist/identity/auth-service.d.ts +0 -118
- package/dist/identity/auth-service.js +0 -170
- package/dist/identity/authorize.d.ts +0 -90
- package/dist/identity/authorize.js +0 -94
- package/dist/identity/grant-service.d.ts +0 -198
- package/dist/identity/grant-service.js +0 -361
- package/dist/identity/hasher.d.ts +0 -39
- package/dist/identity/hasher.js +0 -39
- package/dist/identity/index.d.ts +0 -62
- package/dist/identity/index.js +0 -54
- package/dist/identity/password-policy.d.ts +0 -57
- package/dist/identity/password-policy.js +0 -66
- package/dist/identity/permission-migrations.d.ts +0 -86
- package/dist/identity/permission-migrations.js +0 -67
- package/dist/identity/permissions.d.ts +0 -35
- package/dist/identity/permissions.js +0 -603
- package/dist/identity/ports.d.ts +0 -201
- package/dist/identity/repo.memory.d.ts +0 -164
- package/dist/identity/repo.memory.js +0 -224
- package/dist/identity/seed.d.ts +0 -54
- package/dist/identity/seed.js +0 -429
- package/dist/identity/tool-registrations.d.ts +0 -50
- package/dist/identity/tool-registrations.js +0 -419
- package/dist/identity/types.d.ts +0 -185
- package/dist/identity/types.js +0 -64
- package/dist/identity/username.d.ts +0 -12
- package/dist/identity/username.js +0 -14
- /package/dist/{identity → media/import}/ports.js +0 -0
|
@@ -1,49 +1,48 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { InvalidFieldKindError, InvalidFieldNameGrammarError, InvalidKeyGrammarError } from "./errors.js";
|
|
3
2
|
import { INDEXABLE_FIELD_KINDS, isIndexableFieldKind, } from "./types.js";
|
|
4
3
|
/**
|
|
5
|
-
* @file
|
|
4
|
+
* @file the `kind`->`CAST` fixed lookup table, the identifier grammar
|
|
6
5
|
* gate, the workspace-scoped queryable-index naming scheme, and the before/after index-transition
|
|
7
6
|
* resolver (grammar-and-workspace-scoping folds).
|
|
8
7
|
*
|
|
9
8
|
* Purpose:
|
|
10
9
|
* THIS IS THE HIGHEST-SECURITY-SEVERITY MODULE IN THE 5-PACKAGE PIPELINE — every value that ends
|
|
11
|
-
* up inside a `CREATE INDEX
|
|
10
|
+
* up inside a `CREATE INDEX... CAST(json_extract(fields,'$.ext.{ns}.{field}') AS {type})`
|
|
12
11
|
* statement is produced here, and only here:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
12
|
+
* - `mapFieldKindToCast`. STATE THE INVARIANT PRECISELY, because an earlier version
|
|
13
|
+
* of this comment did not and the imprecision was itself a hazard: it said "a fixed, hardcoded
|
|
14
|
+
* 5-entry table", which reads as though the ENTRY COUNT were the defense. It is not. Neither
|
|
15
|
+
* the number of keys nor the number of distinct values is the security boundary.
|
|
16
|
+
*
|
|
17
|
+
* THE DEFENSE IS: this function returns a VALUE FROM THE TABLE and never returns, echoes, or
|
|
18
|
+
* interpolates its own ARGUMENT. The function has exactly one `return`, and it is a table
|
|
19
|
+
* lookup. Therefore the set of strings that can reach a `CAST(... AS {type})` position is
|
|
20
|
+
* exactly the set of string literals written into `KIND_TO_CAST_LITERAL` in this source file —
|
|
21
|
+
* compile-time constants, every one — no matter what an operator, an agent, or an attacker
|
|
22
|
+
* supplies as `kind`. A 50-entry table of literals would be exactly as safe as a 5-entry one.
|
|
23
|
+
*
|
|
24
|
+
* What follows from that, and what a maintainer actually needs to know:
|
|
25
|
+
* * Adding a KEY whose value is an existing literal cannot widen the DDL alphabet. It is not
|
|
26
|
+
* a security event and does not need to be feared as one. (`relation: "TEXT"` is exactly
|
|
27
|
+
* this: `"TEXT"` was already present twice.)
|
|
28
|
+
* * Adding a new VALUE does widen it, and must be reviewed as a DDL change.
|
|
29
|
+
* * Returning, concatenating, or template-interpolating `kind` itself — or reading the table
|
|
30
|
+
* with a fallback such as `?? kind` — BREAKS the invariant outright. That is the edit to
|
|
31
|
+
* refuse, and it is the one the old "5-entry" phrasing gave no way to recognise.
|
|
32
|
+
* The reachable-value set is pinned executably by `HISTORICAL_DDL_ALPHABET` in
|
|
33
|
+
* `__tests__/field-kind-widening.test.ts`, so this claim is a failing test and not only prose.
|
|
34
|
+
*
|
|
35
|
+
* Degraded failure mode, stated so the blast radius is known: if every runtime guard below
|
|
36
|
+
* were deleted, a storage-only kind would reach the table and `KIND_TO_CAST_LITERAL["json"]`
|
|
37
|
+
* would evaluate to `undefined` — a SQL syntax error and a loud broken feature, never an
|
|
38
|
+
* operator-controlled string. Total guard removal degrades this module to unavailable, not to
|
|
39
|
+
* injectable. That is the property worth preserving through any future change.
|
|
40
|
+
* - `validateIdentifierGrammar`/`buildQueryableFieldIndexName` gate every `key`/field name
|
|
41
|
+
* through `^[a-z][a-z0-9_]{0,63}$` before it can reach an index-name segment or JSON-path
|
|
42
|
+
* literal, and join grammar-gated segments with `/` — a delimiter outside the
|
|
43
|
+
* grammar's own alphabet `[a-z0-9_]`, closing the namespace-injectivity collision class
|
|
44
|
+
* — and fold in a workspace-derived segment so two workspaces defining the same
|
|
45
|
+
* `(key, field)` pair never collide on index identity.
|
|
47
46
|
*
|
|
48
47
|
* How it relates to the project:
|
|
49
48
|
* `resolveFieldIndexTransition` deliberately still ACCEPTS a storage-only kind on its `none` and
|
|
@@ -53,35 +52,39 @@ import { INDEXABLE_FIELD_KINDS, isIndexableFieldKind, } from "./types.js";
|
|
|
53
52
|
*
|
|
54
53
|
* `write-service.ts`'s `registerContentType`/`updateContentTypeFields` call
|
|
55
54
|
* `resolveFieldIndexTransition` once per field (never two independent kind/queryable branches —
|
|
56
|
-
*
|
|
55
|
+
* ) and hand the result to the injected `indexProvisioner` port, which is the only
|
|
57
56
|
* thing that ever issues the real `CREATE INDEX`/`DROP INDEX` DDL (this module only decides what
|
|
58
57
|
* to build, never executes DDL itself).
|
|
59
58
|
*
|
|
60
59
|
* Architectural role:
|
|
61
|
-
* `features/content-types` domain logic.
|
|
62
|
-
*
|
|
60
|
+
* `features/content-types` domain logic. Workspace-segment hashing uses Web Crypto so this
|
|
61
|
+
* universal entry needs only standard runtime APIs and this package's own `errors.ts`/`types.ts`.
|
|
62
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
63
63
|
*/
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* — the closed identifier grammar every `content_types.key` and field name must satisfy,
|
|
66
66
|
* as a pattern string so the agent-facing JSON Schemas in `agent-tools.ts` can publish the very
|
|
67
67
|
* same grammar instead of restating it. A cross-cutting single-source-of-truth governance rule makes this grammar load-bearing for DDL
|
|
68
68
|
* safety, so it must have exactly one definition; {@link IDENTIFIER_GRAMMAR} is compiled from this
|
|
69
69
|
* string rather than written twice.
|
|
70
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
70
71
|
*/
|
|
71
72
|
export const IDENTIFIER_GRAMMAR_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
|
|
72
|
-
/**
|
|
73
|
+
/** — the closed identifier grammar every `content_types.key` and field name must satisfy. See docs/decisions/DR-001-safe-schema-and-index-transitions.md. */
|
|
73
74
|
const IDENTIFIER_GRAMMAR = new RegExp(IDENTIFIER_GRAMMAR_PATTERN);
|
|
74
75
|
/**
|
|
75
|
-
* Structural grammar gate for a `content_types.key` or field name
|
|
76
|
+
* Structural grammar gate for a `content_types.key` or field name.
|
|
76
77
|
*
|
|
77
78
|
* @complexity O(n) in the string's length (regex match), bounded at 64 chars.
|
|
78
79
|
* @overallScore 100
|
|
80
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
79
81
|
*/
|
|
80
|
-
export function validateIdentifierGrammar(
|
|
82
|
+
export function validateIdentifierGrammar(requiredArgs, optionalArgs = {}) {
|
|
83
|
+
const { value } = requiredArgs;
|
|
81
84
|
return IDENTIFIER_GRAMMAR.test(value);
|
|
82
85
|
}
|
|
83
86
|
/**
|
|
84
|
-
*
|
|
87
|
+
* — the fixed, hardcoded `kind` -> SQL `CAST` type-token table. Every value here is a
|
|
85
88
|
* plain uppercase SQL type token with no quotes/parens/whitespace; never built by concatenation.
|
|
86
89
|
*
|
|
87
90
|
* Keyed by `IndexableFieldKind`, NOT `ContentTypeFieldKind`: a storage-only kind has no CAST
|
|
@@ -90,6 +93,7 @@ export function validateIdentifierGrammar(value) {
|
|
|
90
93
|
* compiler-enforced: adding a member to `INDEXABLE_FIELD_KINDS` fails the build until an entry
|
|
91
94
|
* appears here. Do not replace it with a `Partial`, an index signature, or a lookup with a
|
|
92
95
|
* fallback — each of those converts a build failure into a runtime surprise on the DDL path.
|
|
96
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
93
97
|
*/
|
|
94
98
|
const KIND_TO_CAST_LITERAL = {
|
|
95
99
|
text: "TEXT",
|
|
@@ -109,7 +113,7 @@ const KIND_TO_CAST_LITERAL = {
|
|
|
109
113
|
* {@link InvalidFieldKindError} for anything that is not exactly one of `INDEXABLE_FIELD_KINDS` —
|
|
110
114
|
* including a storage-only kind such as `json`, which is a legal kind to declare but has no CAST
|
|
111
115
|
* target. This is a lookup, never a template, so an adversarial payload can never reach the
|
|
112
|
-
* returned string
|
|
116
|
+
* returned string.
|
|
113
117
|
*
|
|
114
118
|
* The PARAMETER type is deliberately the wider `ContentTypeFieldKind`. Narrowing it would be a
|
|
115
119
|
* breaking change for `@jini-ai/cms`'s public consumers (`content-types/index.ts` documents this
|
|
@@ -123,12 +127,16 @@ const KIND_TO_CAST_LITERAL = {
|
|
|
123
127
|
*
|
|
124
128
|
* @complexity O(1) — a fixed-size object lookup behind a fixed-size membership test.
|
|
125
129
|
* @overallScore 100
|
|
130
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
126
131
|
*/
|
|
127
|
-
export function mapFieldKindToCast(
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
export function mapFieldKindToCast(requiredArgs, optionalArgs = {}) {
|
|
133
|
+
const { kind } = requiredArgs;
|
|
134
|
+
const candidate = { value: kind };
|
|
135
|
+
if (!isIndexableFieldKind(candidate)) {
|
|
136
|
+
// See docs/decisions/DR-001-safe-schema-and-index-transitions.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
|
|
137
|
+
throw new InvalidFieldKindError({ message: `'${String(kind)}' is not an indexable field kind (${INDEXABLE_FIELD_KINDS.join("|")})` });
|
|
130
138
|
}
|
|
131
|
-
return KIND_TO_CAST_LITERAL[
|
|
139
|
+
return KIND_TO_CAST_LITERAL[candidate.value];
|
|
132
140
|
}
|
|
133
141
|
/**
|
|
134
142
|
* Derives a grammar-safe, workspace-scoped index-name segment from an arbitrary `workspaceId`
|
|
@@ -136,31 +144,44 @@ export function mapFieldKindToCast(kind) {
|
|
|
136
144
|
* it to a fixed `[0-9a-f]` alphabet (a strict subset of the identifier grammar's `[a-z0-9_]`)
|
|
137
145
|
* prefixed with a letter so the segment independently satisfies {@link validateIdentifierGrammar}.
|
|
138
146
|
*
|
|
139
|
-
*
|
|
147
|
+
* UTF-8 encoding, SHA-256, lowercase hex, and the first 16 hex characters preserve the
|
|
148
|
+
* historical Node hash byte-for-byte, including replacement of unpaired UTF-16 surrogates.
|
|
149
|
+
* Requires Web Crypto; digest failures reject the returned promise.
|
|
150
|
+
*
|
|
151
|
+
* @complexity O(n) time and space in the UTF-8 workspace ID length.
|
|
140
152
|
* @overallScore 100
|
|
141
153
|
*/
|
|
142
|
-
function workspaceIndexSegment(workspaceId) {
|
|
143
|
-
const
|
|
154
|
+
async function workspaceIndexSegment(workspaceId) {
|
|
155
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(workspaceId));
|
|
156
|
+
const digest = Array.from(new Uint8Array(hash).subarray(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
144
157
|
return `w${digest}`;
|
|
145
158
|
}
|
|
146
159
|
/**
|
|
147
160
|
* Builds the workspace-scoped queryable-field index identity for `(workspaceId, contentTypeKey,
|
|
148
161
|
* fieldName)`. Both `contentTypeKey` and `fieldName` are grammar-gated first — a failure throws
|
|
149
|
-
* before any index name is produced
|
|
162
|
+
* before any index name is produced. Segments are joined with `/`, a delimiter outside
|
|
150
163
|
* the grammar's own alphabet `[a-z0-9_]`, so distinct `(key, name)` pairs can never collide by
|
|
151
|
-
* boundary-shifting (
|
|
164
|
+
* boundary-shifting (e.g. `("a_b","c")` vs `("a","b_c")`).
|
|
165
|
+
*
|
|
166
|
+
* Returns a promise for the unchanged index identity. Grammar failures still throw
|
|
167
|
+
* synchronously before hashing; Web Crypto failures reject the promise. Callers must await
|
|
168
|
+
* the name before using it in DDL.
|
|
152
169
|
*
|
|
153
|
-
* @complexity O(
|
|
170
|
+
* @complexity O(n) time and space in the UTF-8 workspace ID length, plus two grammar checks.
|
|
154
171
|
* @overallScore 100
|
|
172
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
155
173
|
*/
|
|
156
174
|
export function buildQueryableFieldIndexName(params) {
|
|
157
|
-
if (!validateIdentifierGrammar(params.contentTypeKey)) {
|
|
158
|
-
|
|
175
|
+
if (!validateIdentifierGrammar({ value: params.contentTypeKey })) {
|
|
176
|
+
// See docs/decisions/DR-001-safe-schema-and-index-transitions.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
|
|
177
|
+
throw new InvalidKeyGrammarError({ message: `content-type key '${params.contentTypeKey}' fails the identifier grammar gate ^[a-z][a-z0-9_]{0,63}$` });
|
|
159
178
|
}
|
|
160
|
-
if (!validateIdentifierGrammar(params.fieldName)) {
|
|
161
|
-
|
|
179
|
+
if (!validateIdentifierGrammar({ value: params.fieldName })) {
|
|
180
|
+
// See docs/decisions/DR-001-safe-schema-and-index-transitions.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
|
|
181
|
+
throw new InvalidFieldNameGrammarError({ message: `field name '${params.fieldName}' fails the identifier grammar gate ^[a-z][a-z0-9_]{0,63}$` });
|
|
162
182
|
}
|
|
163
|
-
|
|
183
|
+
const { workspaceId, contentTypeKey, fieldName } = params;
|
|
184
|
+
return workspaceIndexSegment(workspaceId).then((segment) => `q/${segment}/${contentTypeKey}/${fieldName}`);
|
|
164
185
|
}
|
|
165
186
|
/**
|
|
166
187
|
* Narrows a post-call field kind for the two arms that hand a kind to the index provisioner.
|
|
@@ -174,19 +195,22 @@ export function buildQueryableFieldIndexName(params) {
|
|
|
174
195
|
* @overallScore 100
|
|
175
196
|
*/
|
|
176
197
|
function assertIndexableForProvisioning(kind) {
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
const candidate = { value: kind };
|
|
199
|
+
if (!isIndexableFieldKind(candidate)) {
|
|
200
|
+
// See docs/decisions/DR-001-safe-schema-and-index-transitions.md for the invariant behind this refusal; keep external identifiers out of caller-facing messages.
|
|
201
|
+
throw new InvalidFieldKindError({ message: `storage-only kind '${String(kind)}' reached index provisioning; only (${INDEXABLE_FIELD_KINDS.join("|")}) may be queryable` });
|
|
179
202
|
}
|
|
180
|
-
return
|
|
203
|
+
return candidate.value;
|
|
181
204
|
}
|
|
182
205
|
/**
|
|
183
|
-
*
|
|
206
|
+
* — resolves a single field's index transition from ONE before/after comparison of
|
|
184
207
|
* its `(kind, queryable)` pair, never from two independent branches that each assume the other
|
|
185
208
|
* property is unchanged (the bug class this closes: a field whose `kind` AND `queryable` both
|
|
186
|
-
* change in the same call must still resolve correctly —
|
|
209
|
+
* change in the same call must still resolve correctly — (b)).
|
|
187
210
|
*
|
|
188
211
|
* @complexity O(1) — four comparisons, one of four fixed outcomes.
|
|
189
212
|
* @overallScore 100
|
|
213
|
+
* See docs/decisions/DR-001-safe-schema-and-index-transitions.md.
|
|
190
214
|
*/
|
|
191
215
|
export function resolveFieldIndexTransition(params) {
|
|
192
216
|
const beforeQueryable = params.before?.queryable ?? false;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* See `../entries/index.js`'s header for why these two ship as separate subpaths rather than one.
|
|
8
8
|
*/
|
|
9
|
-
export type { ContentTypeFieldKind, IndexableFieldKind, StorageOnlyFieldKind, ContentTypeFieldDef, ContentTypeStatus, ContentTypeRecord, ActorPrincipalKind, ActorIdentityInput,
|
|
9
|
+
export type { ContentTypeFieldKind, IndexableFieldKind, StorageOnlyFieldKind, ContentTypeFieldDef, ContentTypeStatus, ContentTypeRecord, ActorPrincipalKind, ActorIdentityInput, } from "./types.js";
|
|
10
10
|
/**
|
|
11
11
|
* `CONTENT_TYPE_FIELD_KINDS`/`isContentTypeFieldKind` answer "may this kind be DECLARED and
|
|
12
12
|
* STORED". `INDEXABLE_FIELD_KINDS`/`isIndexableFieldKind` answer the narrower "may this kind be
|
|
@@ -36,7 +36,8 @@ StorageOnlyFieldNotQueryableError, InvalidFieldShapeError, QueryableFieldCapExce
|
|
|
36
36
|
* single definition of the identifier grammar — the published agent-tool schemas reference it
|
|
37
37
|
* rather than restating it, and a consumer validating input ahead of a call must reach the same
|
|
38
38
|
* one. `mapFieldKindToCast`/`buildQueryableFieldIndexName` are what a host's own DDL provisioner
|
|
39
|
-
* needs in order to generate the same index names this module expects to find.
|
|
39
|
+
* needs in order to generate the same index names this module expects to find. The index-name
|
|
40
|
+
* builder returns a promise: await its Web Crypto hash before passing the name to DDL.
|
|
40
41
|
*/
|
|
41
42
|
export type { FieldIndexState, FieldIndexTransition } from "./index-provisioning.js";
|
|
42
43
|
export { IDENTIFIER_GRAMMAR_PATTERN, validateIdentifierGrammar, mapFieldKindToCast, buildQueryableFieldIndexName, resolveFieldIndexTransition, } from "./index-provisioning.js";
|
|
@@ -60,7 +61,7 @@ export { planCleanup, executeCleanup } from "./cleanup.js";
|
|
|
60
61
|
*/
|
|
61
62
|
export { InMemoryContentTypeRepo, NoopContentTypeIndexProvisioner, toContentTypeOutbox, } from "./repo.memory.js";
|
|
62
63
|
/** The agent-tool surface for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
63
|
-
export { contentTypesAgentToolCatalog,
|
|
64
|
+
export { contentTypesAgentToolCatalog, } from "./agent-tools.js";
|
|
64
65
|
/**
|
|
65
66
|
* The agent-tool wiring for this domain.
|
|
66
67
|
*
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Clock } from "@jini-ai/core/primitives";
|
|
2
2
|
import type { AuthorizeFn, ContentTypeRepoPort, OutboxPort } from "./write-service.js";
|
|
3
3
|
import type { TeardownIndexProvisionerPort } from "./lifecycle.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { Result } from "@jini-ai/core/primitives";
|
|
5
|
+
import type { ActorPrincipalKind, ContentTypeRecord } from "./types.js";
|
|
5
6
|
/**
|
|
6
7
|
* @file Content-type lifecycle `op` dispatch (the closed-union-dispatch convention,
|
|
7
8
|
* `features/settings/definitions-dispatch.ts`'s pattern reused verbatim for this domain).
|
|
@@ -15,10 +16,12 @@ import type { ActorPrincipalKind, ContentTypeRecord, Result } from "./types.js";
|
|
|
15
16
|
export declare const CONTENT_TYPE_LIFECYCLE_OP_NAMES: readonly ["deprecate", "reactivate", "tombstone"];
|
|
16
17
|
export type ContentTypeLifecycleOp = (typeof CONTENT_TYPE_LIFECYCLE_OP_NAMES)[number];
|
|
17
18
|
/** Narrows an untrusted `op` string to {@link ContentTypeLifecycleOp}, or `null` if it isn't one. */
|
|
18
|
-
export declare function parseContentTypeLifecycleOp(
|
|
19
|
+
export declare function parseContentTypeLifecycleOp(requiredArgs: {
|
|
20
|
+
op: unknown;
|
|
21
|
+
}, optionalArgs?: Record<string, never>): ContentTypeLifecycleOp | null;
|
|
19
22
|
export interface LifecycleDispatchDeps {
|
|
20
23
|
repo: ContentTypeRepoPort;
|
|
21
|
-
clock:
|
|
24
|
+
clock: Clock;
|
|
22
25
|
authorize: AuthorizeFn;
|
|
23
26
|
outbox: OutboxPort;
|
|
24
27
|
indexProvisioner: TeardownIndexProvisionerPort;
|
|
@@ -31,7 +34,10 @@ export interface LifecycleDispatchInput {
|
|
|
31
34
|
/** Threaded straight through to {@link LifecycleTransitionInput.principalKind} — the audit provenance stamped on the revision row. */
|
|
32
35
|
principalKind?: ActorPrincipalKind;
|
|
33
36
|
}
|
|
34
|
-
export type ContentTypeLifecycleHandler = (
|
|
37
|
+
export type ContentTypeLifecycleHandler = (required: {
|
|
38
|
+
deps: LifecycleDispatchDeps;
|
|
39
|
+
input: LifecycleDispatchInput;
|
|
40
|
+
}, optional?: Record<string, never>) => Promise<Result<{
|
|
35
41
|
contentType: ContentTypeRecord;
|
|
36
42
|
}, Error>>;
|
|
37
43
|
export declare const CONTENT_TYPE_LIFECYCLE_OPS: Record<ContentTypeLifecycleOp, ContentTypeLifecycleHandler>;
|
|
@@ -11,7 +11,8 @@ import { deprecateContentType, reactivateContentType, tombstoneContentType } fro
|
|
|
11
11
|
*/
|
|
12
12
|
export const CONTENT_TYPE_LIFECYCLE_OP_NAMES = ["deprecate", "reactivate", "tombstone"];
|
|
13
13
|
/** Narrows an untrusted `op` string to {@link ContentTypeLifecycleOp}, or `null` if it isn't one. */
|
|
14
|
-
export function parseContentTypeLifecycleOp(
|
|
14
|
+
export function parseContentTypeLifecycleOp(requiredArgs, optionalArgs = {}) {
|
|
15
|
+
const { op } = requiredArgs;
|
|
15
16
|
return typeof op === "string" && CONTENT_TYPE_LIFECYCLE_OP_NAMES.includes(op)
|
|
16
17
|
? op
|
|
17
18
|
: null;
|
|
@@ -20,9 +21,9 @@ export function parseContentTypeLifecycleOp(op) {
|
|
|
20
21
|
* (defense-in-depth against a prototype-chain key ever resolving, matching the settings
|
|
21
22
|
* dispatch table's own convention). */
|
|
22
23
|
const contentTypeLifecycleOps = {
|
|
23
|
-
deprecate: (deps, input) => deprecateContentType({ deps, input }),
|
|
24
|
-
reactivate: (deps, input) => reactivateContentType({ deps: { repo: deps.repo, clock: deps.clock, authorize: deps.authorize }, input }),
|
|
25
|
-
tombstone: (deps, input) => tombstoneContentType({ deps, input }),
|
|
24
|
+
deprecate: ({ deps, input }) => deprecateContentType({ deps, input }),
|
|
25
|
+
reactivate: ({ deps, input }) => reactivateContentType({ deps: { repo: deps.repo, clock: deps.clock, authorize: deps.authorize }, input }),
|
|
26
|
+
tombstone: ({ deps, input }) => tombstoneContentType({ deps, input }),
|
|
26
27
|
};
|
|
27
28
|
export const CONTENT_TYPE_LIFECYCLE_OPS = Object.assign(Object.create(null), contentTypeLifecycleOps);
|
|
28
29
|
//# sourceMappingURL=lifecycle-dispatch.js.map
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Clock } from "@jini-ai/core/primitives";
|
|
2
2
|
import type { AuthorizeFn, ContentTypeRepoPort, OutboxPort } from "./write-service.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Result } from "@jini-ai/core/primitives";
|
|
4
|
+
import type { ActorPrincipalKind, ContentTypeRecord } from "./types.js";
|
|
4
5
|
/**
|
|
5
|
-
* @file
|
|
6
|
-
* tombstone
|
|
6
|
+
* @file..12 — the content-type lifecycle state machine `active ⇄ deprecated ->
|
|
7
|
+
* tombstone`.
|
|
7
8
|
*
|
|
8
9
|
* Purpose:
|
|
9
10
|
* `deprecateContentType`/`reactivateContentType` are the reversible `active <-> deprecated` pair
|
|
10
|
-
* (deprecate blocks new-entry creation only, per
|
|
11
|
+
* (deprecate blocks new-entry creation only, per asymmetry — existing entries stay
|
|
11
12
|
* fully readable/writable). `tombstoneContentType` is the one-way terminal transition: it MUST be
|
|
12
|
-
* entered from `deprecated` (
|
|
13
|
+
* entered from `deprecated` ("must deprecate first") and never left — both
|
|
13
14
|
* `reactivateContentType` and `deprecateContentType` reject unconditionally against a tombstoned
|
|
14
|
-
* row (the property `operationTests
|
|
15
|
-
* index the type ever provisioned
|
|
15
|
+
* row (the property `operationTests ` case). Tombstoning tears down every queryable-field
|
|
16
|
+
* index the type ever provisioned and enqueues the same-call outbox event
|
|
16
17
|
* required for `content_type.deprecated`/`content_type.tombstoned`.
|
|
17
18
|
*
|
|
18
19
|
* How it relates to the project:
|
|
19
20
|
* `reactivateContentType` deliberately does not accept `outbox`/`indexProvisioner` deps — no
|
|
20
21
|
* certified test requires a reactivation event or index reprovisioning on this transition, and
|
|
21
|
-
*
|
|
22
|
+
* 12 name only deprecate/tombstone as outbox-obligated.
|
|
22
23
|
*
|
|
23
24
|
* Architectural role:
|
|
24
25
|
* `features/content-types` domain logic, composing `write-service.ts`'s repo/outbox/index-
|
|
25
26
|
* provisioner port shapes rather than redeclaring them.
|
|
27
|
+
* See docs/decisions/DR-002-content-lifecycle-and-cleanup.md.
|
|
26
28
|
*/
|
|
27
29
|
export interface LifecycleTransitionInput {
|
|
28
30
|
workspaceId: string;
|
|
@@ -35,19 +37,20 @@ export interface LifecycleTransitionInput {
|
|
|
35
37
|
export interface DeprecateContentTypeRequired {
|
|
36
38
|
deps: {
|
|
37
39
|
repo: ContentTypeRepoPort;
|
|
38
|
-
clock:
|
|
40
|
+
clock: Clock;
|
|
39
41
|
authorize: AuthorizeFn;
|
|
40
42
|
outbox: OutboxPort;
|
|
41
43
|
};
|
|
42
44
|
input: LifecycleTransitionInput;
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
|
-
*
|
|
46
|
-
* enqueues `content_type.deprecated` in the same call. Rejected unconditionally
|
|
47
|
+
* — flips an active type to `deprecated` (blocks only NEW entry creation) and
|
|
48
|
+
* enqueues `content_type.deprecated` in the same call. Rejected unconditionally if the
|
|
47
49
|
* type is already `tombstone` — deprecate can never be used to "step back" out of tombstone.
|
|
48
50
|
*
|
|
49
51
|
* @complexity O(1) plus one repo read, one same-tx write pair, and one outbox enqueue.
|
|
50
52
|
* @overallScore 100
|
|
53
|
+
* See docs/decisions/DR-002-content-lifecycle-and-cleanup.md.
|
|
51
54
|
*/
|
|
52
55
|
export declare function deprecateContentType(required: DeprecateContentTypeRequired): Promise<Result<{
|
|
53
56
|
contentType: ContentTypeRecord;
|
|
@@ -55,17 +58,18 @@ export declare function deprecateContentType(required: DeprecateContentTypeRequi
|
|
|
55
58
|
export interface ReactivateContentTypeRequired {
|
|
56
59
|
deps: {
|
|
57
60
|
repo: ContentTypeRepoPort;
|
|
58
|
-
clock:
|
|
61
|
+
clock: Clock;
|
|
59
62
|
authorize: AuthorizeFn;
|
|
60
63
|
};
|
|
61
64
|
input: LifecycleTransitionInput;
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
|
-
*
|
|
65
|
-
* `active`. Rejected unconditionally
|
|
67
|
+
* — flips a `deprecated` (or already-`active`, a no-op-shaped success) type back to
|
|
68
|
+
* `active`. Rejected unconditionally against a tombstoned row.
|
|
66
69
|
*
|
|
67
70
|
* @complexity O(1) plus one repo read and one same-tx write pair.
|
|
68
71
|
* @overallScore 100
|
|
72
|
+
* See docs/decisions/DR-002-content-lifecycle-and-cleanup.md.
|
|
69
73
|
*/
|
|
70
74
|
export declare function reactivateContentType(required: ReactivateContentTypeRequired): Promise<Result<{
|
|
71
75
|
contentType: ContentTypeRecord;
|
|
@@ -80,7 +84,7 @@ export interface TeardownIndexProvisionerPort {
|
|
|
80
84
|
export interface TombstoneContentTypeRequired {
|
|
81
85
|
deps: {
|
|
82
86
|
repo: ContentTypeRepoPort;
|
|
83
|
-
clock:
|
|
87
|
+
clock: Clock;
|
|
84
88
|
authorize: AuthorizeFn;
|
|
85
89
|
outbox: OutboxPort;
|
|
86
90
|
indexProvisioner: TeardownIndexProvisionerPort;
|
|
@@ -88,15 +92,16 @@ export interface TombstoneContentTypeRequired {
|
|
|
88
92
|
input: LifecycleTransitionInput;
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
91
|
-
*
|
|
95
|
+
* — the one-way terminal transition into `tombstone`. Must be entered from
|
|
92
96
|
* `deprecated` (any other status, including an already-tombstoned row, is rejected with the same
|
|
93
|
-
* "must deprecate first" guard —
|
|
97
|
+
* "must deprecate first" guard — collapse to one check). Tears down every queryable-
|
|
94
98
|
* field index the type ever provisioned BEFORE persisting the status flip, and enqueues
|
|
95
99
|
* `content_type.tombstoned` in the same call.
|
|
96
100
|
*
|
|
97
101
|
* @complexity O(1) plus one repo read, one index-teardown call, one same-tx write pair, and one
|
|
98
102
|
* outbox enqueue.
|
|
99
103
|
* @overallScore 100
|
|
104
|
+
* See docs/decisions/DR-002-content-lifecycle-and-cleanup.md.
|
|
100
105
|
*/
|
|
101
106
|
export declare function tombstoneContentType(required: TombstoneContentTypeRequired): Promise<Result<{
|
|
102
107
|
contentType: ContentTypeRecord;
|