@jini-ai/core 0.4.1 → 0.4.3

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 (131) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +19 -1
  3. package/dist/agent-tools.d.ts +1 -0
  4. package/dist/approval.d.ts +134 -0
  5. package/dist/approval.js +141 -0
  6. package/dist/fetch/cache.d.ts +129 -0
  7. package/dist/fetch/cache.js +243 -0
  8. package/dist/fetch/index.d.ts +5 -0
  9. package/dist/fetch/index.js +3 -0
  10. package/dist/fetch/types.d.ts +93 -0
  11. package/dist/fetch/types.js +2 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +6 -1
  14. package/dist/model-facing-tool-errors.d.ts +152 -0
  15. package/dist/model-facing-tool-errors.js +124 -0
  16. package/dist/native-regexp.d.ts +10 -0
  17. package/dist/native-regexp.js +2 -0
  18. package/dist/primitives/index.d.ts +2 -0
  19. package/dist/primitives/index.js +1 -0
  20. package/dist/primitives/sse.d.ts +60 -0
  21. package/dist/primitives/sse.js +122 -0
  22. package/dist/redact.js +4 -3
  23. package/dist/registration-kit.d.ts +13 -0
  24. package/dist/registration-kit.js +12 -0
  25. package/dist/settings/agent-tools.d.ts +10 -0
  26. package/dist/settings/agent-tools.js +204 -0
  27. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  28. package/dist/settings/agent-value-write-tools.js +157 -0
  29. package/dist/settings/agent-writable-preferences.d.ts +67 -0
  30. package/dist/settings/agent-writable-preferences.js +132 -0
  31. package/dist/settings/agent-write-denylist.d.ts +19 -0
  32. package/dist/settings/agent-write-denylist.js +15 -0
  33. package/dist/settings/change-feed.d.ts +83 -0
  34. package/dist/settings/change-feed.js +61 -0
  35. package/dist/settings/definitions-dispatch.d.ts +56 -0
  36. package/dist/settings/definitions-dispatch.js +86 -0
  37. package/dist/settings/dictionaries/index.d.ts +48 -0
  38. package/dist/settings/dictionaries/index.js +69 -0
  39. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
  40. package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
  41. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
  42. package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
  43. package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
  44. package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
  45. package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
  46. package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
  47. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
  48. package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
  49. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
  50. package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
  51. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
  52. package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
  53. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
  54. package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
  55. package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
  56. package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
  57. package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
  58. package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
  59. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
  60. package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
  61. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
  62. package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
  63. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
  64. package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
  65. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
  66. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
  67. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
  68. package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
  69. package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
  70. package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
  71. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
  72. package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
  73. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
  74. package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
  75. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
  76. package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
  77. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
  78. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
  79. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
  80. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
  81. package/dist/settings/ensure-definitions.d.ts +101 -0
  82. package/dist/settings/ensure-definitions.js +101 -0
  83. package/dist/settings/errors.d.ts +42 -0
  84. package/dist/settings/errors.js +46 -0
  85. package/dist/settings/express/cms-adapter.d.ts +20 -0
  86. package/dist/settings/express/cms-adapter.js +124 -0
  87. package/dist/settings/express/contracts.d.ts +135 -0
  88. package/dist/settings/express/contracts.js +2 -0
  89. package/dist/settings/express/http.d.ts +10 -0
  90. package/dist/settings/express/http.js +211 -0
  91. package/dist/settings/express/index.d.ts +5 -0
  92. package/dist/settings/express/index.js +4 -0
  93. package/dist/settings/express/shared.d.ts +68 -0
  94. package/dist/settings/express/shared.js +54 -0
  95. package/dist/settings/express/stream.d.ts +24 -0
  96. package/dist/settings/express/stream.js +147 -0
  97. package/dist/settings/index.d.ts +40 -0
  98. package/dist/settings/index.js +37 -0
  99. package/dist/settings/ports.d.ts +118 -0
  100. package/dist/settings/ports.js +2 -0
  101. package/dist/settings/principal-lookup.d.ts +9 -0
  102. package/dist/settings/principal-lookup.js +2 -0
  103. package/dist/settings/purge-service.d.ts +54 -0
  104. package/dist/settings/purge-service.js +106 -0
  105. package/dist/settings/repo.memory.d.ts +119 -0
  106. package/dist/settings/repo.memory.js +199 -0
  107. package/dist/settings/settings.d.ts +139 -0
  108. package/dist/settings/settings.js +259 -0
  109. package/dist/settings/tool-registrations.d.ts +54 -0
  110. package/dist/settings/tool-registrations.js +250 -0
  111. package/dist/settings/types.d.ts +125 -0
  112. package/dist/settings/types.js +3 -0
  113. package/dist/settings/ui-tab-definitions.d.ts +41 -0
  114. package/dist/settings/ui-tab-definitions.js +148 -0
  115. package/dist/settings/write-service.d.ts +320 -0
  116. package/dist/settings/write-service.js +683 -0
  117. package/dist/surface-exchanges.d.ts +209 -0
  118. package/dist/surface-exchanges.js +55 -0
  119. package/dist/text/escape.d.ts +20 -0
  120. package/dist/text/escape.js +23 -0
  121. package/dist/text/index.d.ts +1 -0
  122. package/dist/text/index.js +1 -0
  123. package/dist/tool-metadata.d.ts +71 -0
  124. package/dist/tool-metadata.js +56 -0
  125. package/dist/tool-permissions.d.ts +76 -0
  126. package/dist/tool-permissions.js +57 -0
  127. package/dist/tool-registry.d.ts +19 -1
  128. package/dist/tool-registry.js +44 -0
  129. package/package.json +79 -4
  130. package/plugin-directory.d.ts +32 -0
  131. package/plugin-directory.js +140 -0
@@ -0,0 +1,118 @@
1
+ import type { UUID } from "../primitives/index.js";
2
+ import type { SettingDefinitionRecord, SettingRevisionRecord, SettingValueRecord } from "./types.js";
3
+ /**
4
+ * @file Port contract for the `settings` library.
5
+ *
6
+ * Purpose:
7
+ * Dependency-inversion seam (port/adapter rule-of-two) for the 5 settings tables.
8
+ * Persistence deliberately has no principal-lookup method: target-principal checks use
9
+ * the separate host-owned SettingsPrincipalLookupPort, keeping membership/status policy
10
+ * outside the settings persistence contract.
11
+ *
12
+ * `transaction` gives `write-service.ts`/`purge-service.ts` the same-tx
13
+ * guarantee depend on (value/definition row + its revision row
14
+ * must commit together, or not at all).
15
+ *
16
+ * Interfaces and types only — no feature logic.
17
+ * See docs/decisions/DR-003-settings-ledger-invariants.md.
18
+ */
19
+ export interface SettingsRepoPort {
20
+ findActiveDefinition(required: {
21
+ namespace: string;
22
+ key: string;
23
+ workspaceId: UUID | null;
24
+ }): Promise<SettingDefinitionRecord | null>;
25
+ findDefinitionBySettingId(required: {
26
+ settingId: UUID;
27
+ version?: number;
28
+ }): Promise<SettingDefinitionRecord | null>;
29
+ listActiveDefinitions(required: {
30
+ workspaceId: UUID | null;
31
+ }): Promise<SettingDefinitionRecord[]>;
32
+ saveDefinition(record: SettingDefinitionRecord): Promise<void>;
33
+ getGlobalValue(settingId: UUID): Promise<SettingValueRecord | null>;
34
+ getWorkspaceValue(required: {
35
+ workspaceId: UUID;
36
+ settingId: UUID;
37
+ }): Promise<SettingValueRecord | null>;
38
+ getUserValue(required: {
39
+ workspaceId: UUID;
40
+ principalId: UUID;
41
+ settingId: UUID;
42
+ }): Promise<SettingValueRecord | null>;
43
+ saveGlobalValue(record: SettingValueRecord): Promise<void>;
44
+ saveWorkspaceValue(record: SettingValueRecord): Promise<void>;
45
+ saveUserValue(record: SettingValueRecord): Promise<void>;
46
+ listWorkspaceValues(required: {
47
+ workspaceId: UUID;
48
+ }): Promise<SettingValueRecord[]>;
49
+ listUserValues(required: {
50
+ workspaceId: UUID;
51
+ principalId: UUID;
52
+ }): Promise<SettingValueRecord[]>;
53
+ /**
54
+ * Every `setting_values_user` row for a workspace, across ALL principals
55
+ * (not just one) — needed by `purge-service.ts`'s full-tenant teardown path,
56
+ * which must clear the FK-blocking `setting_values_user` rows
57
+ * for every principal in the workspace, not just one caller-known id.
58
+ */
59
+ listUserValuesByWorkspace(required: {
60
+ workspaceId: UUID;
61
+ }): Promise<SettingValueRecord[]>;
62
+ deleteWorkspaceValue(required: {
63
+ workspaceId: UUID;
64
+ settingId: UUID;
65
+ }): Promise<void>;
66
+ deleteUserValue(required: {
67
+ workspaceId: UUID;
68
+ principalId: UUID;
69
+ settingId: UUID;
70
+ }): Promise<void>;
71
+ /** Appends a revision and returns its assigned `seq` (used to stamp the paired value row). */
72
+ appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
73
+ listRevisions(required: {
74
+ settingId: UUID;
75
+ }): Promise<SettingRevisionRecord[]>;
76
+ /**
77
+ * Revisions with `seq` strictly greater than `sinceSeq`, oldest first, capped at `limit`, and
78
+ * restricted to those `workspaceId` could possibly care about.
79
+ *
80
+ * Exists for change detection across PROCESSES. `seq` is a monotonic autoincrement in the shared
81
+ * database, so a poller in one process observes writes made by another process (e.g. an agent
82
+ * daemon) — a separate OS process with its own connection — without any IPC between them.
83
+ *
84
+ * `limit` bounds a client that reconnects after a long absence: it drains in pages rather than
85
+ * loading an unbounded backlog into memory.
86
+ *
87
+ * **`workspaceId` is a page-sizing predicate, not the disclosure boundary.** The ledger is global
88
+ * — one store holds every workspace's revisions — so without it a `limit`-sized page is filled by
89
+ * whichever tenant writes fastest, and a quiet workspace's own change waits behind a busy
90
+ * neighbour's backlog. That is an observable cross-tenant timing channel and, at sustained write
91
+ * rates above `limit` per poll interval, a feed that never catches up at all. Implementations must
92
+ * match rows whose `workspaceId` is either `null` (platform definitions and `global`-scope values,
93
+ * which every workspace resolves through) or equal to the argument — deliberately a SUPERSET of
94
+ * what `change-feed.ts#isRevisionVisibleTo` permits, which stays the sole authority on disclosure.
95
+ */
96
+ listRevisionsSince(required: {
97
+ sinceSeq: number;
98
+ limit: number;
99
+ workspaceId: UUID;
100
+ }): Promise<SettingRevisionRecord[]>;
101
+ /** The newest assigned `seq`, or 0 when the ledger is empty — a subscriber's starting cursor. */
102
+ maxRevisionSeq(): Promise<number>;
103
+ /**
104
+ * Runs `fn` with the guarantee that all repo calls inside it commit or roll
105
+ * back together. Every adapter must honour this — it is what makes a
106
+ * composite write like `resetNamespace` all-or-nothing, and the contract
107
+ * suite asserts it against all of them rather than only the durable one.
108
+ *
109
+ * **Not reentrant.** Calling this while another `transaction` on the same
110
+ * repo is open must fail loudly, never silently join. An adapter cannot tell
111
+ * a genuine nested call from an unrelated concurrent one, and joining a
112
+ * stranger's transaction makes it inherit that transaction's rollback. A
113
+ * caller that needs several writes to commit together opens ONE transaction
114
+ * and passes `skipTransaction` to the inner writes.
115
+ */
116
+ transaction<T>(fn: () => Promise<T>): Promise<T>;
117
+ }
118
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1,9 @@
1
+ import type { Principal } from "../principal.js";
2
+ /** Resolve only an active principal in the requested workspace. The host owns status and membership policy. */
3
+ export interface SettingsPrincipalLookupPort {
4
+ findActiveById(required: {
5
+ workspaceId: string;
6
+ id: string;
7
+ }): Promise<Principal | null>;
8
+ }
9
+ //# sourceMappingURL=principal-lookup.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=principal-lookup.js.map
@@ -0,0 +1,54 @@
1
+ import type { Clock, UUID } from "../primitives/index.js";
2
+ import type { SettingsRepoPort } from "./ports.js";
3
+ import type { AuthorizeFn } from "./write-service.js";
4
+ /**
5
+ * @file `purgeTenantSettings` — the ledgered tenant/principal purge chokepoint.
6
+ *
7
+ * Purpose:
8
+ * A host's value-table FKs (`setting_values_workspace`/`_user` -> `workspaces`)
9
+ * are typically `ON DELETE RESTRICT`, not `CASCADE` — a raw workspace delete
10
+ * cannot silently drop value rows without a ledgered revision. This module is
11
+ * the explicit, ledgered alternative that requires: `authorize` once
12
+ * -> enumerate every affected row -> append a redacted `op='purge'` revision
13
+ * per row -> delete the row, all in ONE transaction. The `setting_revisions`
14
+ * ledger itself is NEVER touched by a purge — only value rows are removed,
15
+ * with their purge revision left behind. Repo write methods stay
16
+ * package-private to this file and `write-service.ts`.
17
+ *
18
+ * Deviation from the outline's `SettingsWriteServiceDeps`: purge needs
19
+ * neither `ids` (no new definition/value rows are created) nor `principals`
20
+ * ( target-principal check is a write-time guard; purge only removes
21
+ * pre-existing rows, it never writes a new user-scope row for an unverified
22
+ * principal) — so it declares its own smaller `PurgeServiceDeps`.
23
+ * See docs/decisions/DR-003-settings-ledger-invariants.md.
24
+ */
25
+ export interface PurgeServiceDeps {
26
+ repo: SettingsRepoPort;
27
+ clock: Clock;
28
+ authorize: AuthorizeFn;
29
+ }
30
+ export interface PurgeTenantSettingsRequired {
31
+ deps: PurgeServiceDeps;
32
+ input: {
33
+ workspaceId: UUID;
34
+ /**
35
+ * Omitted -> full tenant teardown: every `setting_values_workspace` row
36
+ * for the workspace AND every `setting_values_user` row for every
37
+ * principal in the workspace.
38
+ * Present -> scope the purge to one principal's `setting_values_user`
39
+ * rows only (GDPR single-subject erasure) — workspace-scope values and
40
+ * every other principal's rows are left untouched.
41
+ */
42
+ principalId?: UUID;
43
+ callerPrincipalId: UUID;
44
+ };
45
+ }
46
+ /**
47
+ * The purge chokepoint: authorize `settings.definitions.manage` once ->
48
+ * enumerate the affected rows -> append a redacted `op='purge'` revision +
49
+ * delete, per row, all in one transaction.
50
+ */
51
+ export declare function purgeTenantSettings(required: PurgeTenantSettingsRequired): Promise<{
52
+ purgedCount: number;
53
+ }>;
54
+ //# sourceMappingURL=purge-service.d.ts.map
@@ -0,0 +1,106 @@
1
+ import { nowIso as kernelNowIso } from "../primitives/index.js";
2
+ import { ForbiddenError } from "./errors.js";
3
+ import { invalidateWorkspaceSettingsCache } from "./settings.js";
4
+ /**
5
+ * The purge chokepoint: authorize `settings.definitions.manage` once ->
6
+ * enumerate the affected rows -> append a redacted `op='purge'` revision +
7
+ * delete, per row, all in one transaction.
8
+ */
9
+ export async function purgeTenantSettings(required) {
10
+ const { deps, input } = required;
11
+ const authResult = await deps.authorize({
12
+ principalId: input.callerPrincipalId,
13
+ permission: "settings.definitions.manage",
14
+ workspaceId: input.workspaceId,
15
+ entityType: "setting-workspace",
16
+ entityId: input.workspaceId,
17
+ });
18
+ if (!authResult.allowed) {
19
+ throw new ForbiddenError(`principal '${input.callerPrincipalId}' is not authorized for 'settings.definitions.manage' (${authResult.reason})`);
20
+ }
21
+ const result = await deps.repo.transaction(async () => {
22
+ const now = kernelNowIso({ clock: deps.clock });
23
+ let purgedCount = 0;
24
+ if (input.principalId) {
25
+ const userValues = await deps.repo.listUserValues({
26
+ workspaceId: input.workspaceId,
27
+ principalId: input.principalId,
28
+ });
29
+ for (const value of userValues) {
30
+ await deps.repo.appendRevision({
31
+ entityKind: "value",
32
+ settingId: value.settingId,
33
+ scope: "user",
34
+ workspaceId: input.workspaceId,
35
+ principalId: input.principalId,
36
+ op: "purge",
37
+ beforeJson: null,
38
+ afterJson: null,
39
+ defVersion: value.defVersion,
40
+ actor: input.callerPrincipalId,
41
+ originPluginId: null,
42
+ changeSetId: null,
43
+ createdAt: now,
44
+ });
45
+ await deps.repo.deleteUserValue({
46
+ workspaceId: input.workspaceId,
47
+ principalId: input.principalId,
48
+ settingId: value.settingId,
49
+ });
50
+ purgedCount++;
51
+ }
52
+ return { purgedCount };
53
+ }
54
+ const workspaceValues = await deps.repo.listWorkspaceValues({ workspaceId: input.workspaceId });
55
+ for (const value of workspaceValues) {
56
+ await deps.repo.appendRevision({
57
+ entityKind: "value",
58
+ settingId: value.settingId,
59
+ scope: "workspace",
60
+ workspaceId: input.workspaceId,
61
+ principalId: null,
62
+ op: "purge",
63
+ beforeJson: null,
64
+ afterJson: null,
65
+ defVersion: value.defVersion,
66
+ actor: input.callerPrincipalId,
67
+ originPluginId: null,
68
+ changeSetId: null,
69
+ createdAt: now,
70
+ });
71
+ await deps.repo.deleteWorkspaceValue({ workspaceId: input.workspaceId, settingId: value.settingId });
72
+ purgedCount++;
73
+ }
74
+ const userValues = await deps.repo.listUserValuesByWorkspace({ workspaceId: input.workspaceId });
75
+ for (const value of userValues) {
76
+ const targetPrincipalId = value.principalId;
77
+ if (!targetPrincipalId)
78
+ continue; // defensive: every setting_values_user row carries a principal_id
79
+ await deps.repo.appendRevision({
80
+ entityKind: "value",
81
+ settingId: value.settingId,
82
+ scope: "user",
83
+ workspaceId: input.workspaceId,
84
+ principalId: targetPrincipalId,
85
+ op: "purge",
86
+ beforeJson: null,
87
+ afterJson: null,
88
+ defVersion: value.defVersion,
89
+ actor: input.callerPrincipalId,
90
+ originPluginId: null,
91
+ changeSetId: null,
92
+ createdAt: now,
93
+ });
94
+ await deps.repo.deleteUserValue({
95
+ workspaceId: input.workspaceId,
96
+ principalId: targetPrincipalId,
97
+ settingId: value.settingId,
98
+ });
99
+ purgedCount++;
100
+ }
101
+ return { purgedCount };
102
+ });
103
+ invalidateWorkspaceSettingsCache({ repo: deps.repo, workspaceId: input.workspaceId }, { principalId: input.principalId });
104
+ return result;
105
+ }
106
+ //# sourceMappingURL=purge-service.js.map
@@ -0,0 +1,119 @@
1
+ import type { SettingsRepoPort } from "./ports.js";
2
+ import type { SettingDefinitionRecord, SettingRevisionRecord, SettingValueRecord } from "./types.js";
3
+ /**
4
+ * @file In-memory adapter for `SettingsRepoPort` (the rule-of-two: in-memory + production adapters).
5
+ *
6
+ * Purpose:
7
+ * Test/dev implementation. Mirrors the exact style of `identity/repo.memory.ts`:
8
+ * constructor takes seed rows, methods filter/mutate internal arrays.
9
+ *
10
+ * `transaction` is a real snapshot/restore journal, not a no-op wrapper. It
11
+ * used to be `async (fn) => fn()` on the reasoning that each individual
12
+ * mutation is already synchronous and atomic in-process — true, but the
13
+ * guarantee `SettingsRepoPort.transaction` actually sells is that a COMPOSITE
14
+ * of several mutations is all-or-nothing. `resetNamespace` clears a whole
15
+ * namespace inside one transaction; under a no-op wrapper a failure partway
16
+ * through left the namespace half reset while the caller saw an error. This
17
+ * adapter is a common wired default for dev/test composition, so that was
18
+ * live in dev, and the contract suite could not see it because the rollback
19
+ * assertion only ran against the SQLite adapter.
20
+ */
21
+ export declare class InMemorySettingsRepo implements SettingsRepoPort {
22
+ private definitions;
23
+ private globalValues;
24
+ private workspaceValues;
25
+ private userValues;
26
+ private revisions;
27
+ private nextSeq;
28
+ /** True while `transaction` is open — see its doc for why this refuses
29
+ * rather than nests. */
30
+ private inTransaction;
31
+ constructor(seed?: {
32
+ definitions?: SettingDefinitionRecord[];
33
+ globalValues?: SettingValueRecord[];
34
+ workspaceValues?: SettingValueRecord[];
35
+ userValues?: SettingValueRecord[];
36
+ revisions?: SettingRevisionRecord[];
37
+ });
38
+ findActiveDefinition(required: {
39
+ namespace: string;
40
+ key: string;
41
+ workspaceId: string | null;
42
+ }): Promise<SettingDefinitionRecord | null>;
43
+ findDefinitionBySettingId(required: {
44
+ settingId: string;
45
+ version?: number;
46
+ }): Promise<SettingDefinitionRecord | null>;
47
+ listActiveDefinitions(required: {
48
+ workspaceId: string | null;
49
+ }): Promise<SettingDefinitionRecord[]>;
50
+ saveDefinition(record: SettingDefinitionRecord): Promise<void>;
51
+ getGlobalValue(settingId: string): Promise<SettingValueRecord | null>;
52
+ getWorkspaceValue(required: {
53
+ workspaceId: string;
54
+ settingId: string;
55
+ }): Promise<SettingValueRecord | null>;
56
+ getUserValue(required: {
57
+ workspaceId: string;
58
+ principalId: string;
59
+ settingId: string;
60
+ }): Promise<SettingValueRecord | null>;
61
+ saveGlobalValue(record: SettingValueRecord): Promise<void>;
62
+ saveWorkspaceValue(record: SettingValueRecord): Promise<void>;
63
+ saveUserValue(record: SettingValueRecord): Promise<void>;
64
+ listWorkspaceValues(required: {
65
+ workspaceId: string;
66
+ }): Promise<SettingValueRecord[]>;
67
+ listUserValues(required: {
68
+ workspaceId: string;
69
+ principalId: string;
70
+ }): Promise<SettingValueRecord[]>;
71
+ listUserValuesByWorkspace(required: {
72
+ workspaceId: string;
73
+ }): Promise<SettingValueRecord[]>;
74
+ deleteWorkspaceValue(required: {
75
+ workspaceId: string;
76
+ settingId: string;
77
+ }): Promise<void>;
78
+ deleteUserValue(required: {
79
+ workspaceId: string;
80
+ principalId: string;
81
+ settingId: string;
82
+ }): Promise<void>;
83
+ appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
84
+ listRevisions(required: {
85
+ settingId: string;
86
+ }): Promise<SettingRevisionRecord[]>;
87
+ listRevisionsSince(required: {
88
+ sinceSeq: number;
89
+ limit: number;
90
+ workspaceId: string;
91
+ }): Promise<SettingRevisionRecord[]>;
92
+ maxRevisionSeq(): Promise<number>;
93
+ /**
94
+ * Runs `fn` atomically: every mutation it makes is discarded if it throws.
95
+ *
96
+ * A shallow copy of each array is enough because no method here mutates a
97
+ * record in place — `save*` REPLACES the element at its index, `delete*`
98
+ * reassigns a filtered array, `appendRevision` pushes. Restoring the arrays
99
+ * therefore restores the rows too. If a future method starts editing a
100
+ * record object in place, this must deep-copy instead.
101
+ *
102
+ * `nextSeq` is rolled back with the rest, matching SQLite: `sqlite_sequence`
103
+ * is an ordinary table, so an AUTOINCREMENT counter bumped inside a
104
+ * transaction is undone by its ROLLBACK. A seq handed out by a failed
105
+ * transaction is safe to reuse precisely because that revision no longer
106
+ * exists.
107
+ *
108
+ * **Deliberately NOT reentrant**, matching a SQLite adapter's `transaction`,
109
+ * where `BEGIN IMMEDIATE` inside an open transaction is an error. Refusing
110
+ * loudly is the point: an instance-level flag cannot tell "nested inside my
111
+ * caller's transaction" from "an unrelated transaction that started while
112
+ * mine was awaiting". Silently joining a stranger's transaction — and
113
+ * inheriting its rollback — is a worse defect than refusing. A caller that
114
+ * needs several writes to commit together opens ONE transaction and passes
115
+ * `skipTransaction` to the inner writes, as `resetNamespace` does.
116
+ */
117
+ transaction<T>(fn: () => Promise<T>): Promise<T>;
118
+ }
119
+ //# sourceMappingURL=repo.memory.d.ts.map
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @file In-memory adapter for `SettingsRepoPort` (the rule-of-two: in-memory + production adapters).
3
+ *
4
+ * Purpose:
5
+ * Test/dev implementation. Mirrors the exact style of `identity/repo.memory.ts`:
6
+ * constructor takes seed rows, methods filter/mutate internal arrays.
7
+ *
8
+ * `transaction` is a real snapshot/restore journal, not a no-op wrapper. It
9
+ * used to be `async (fn) => fn()` on the reasoning that each individual
10
+ * mutation is already synchronous and atomic in-process — true, but the
11
+ * guarantee `SettingsRepoPort.transaction` actually sells is that a COMPOSITE
12
+ * of several mutations is all-or-nothing. `resetNamespace` clears a whole
13
+ * namespace inside one transaction; under a no-op wrapper a failure partway
14
+ * through left the namespace half reset while the caller saw an error. This
15
+ * adapter is a common wired default for dev/test composition, so that was
16
+ * live in dev, and the contract suite could not see it because the rollback
17
+ * assertion only ran against the SQLite adapter.
18
+ */
19
+ export class InMemorySettingsRepo {
20
+ definitions;
21
+ globalValues;
22
+ workspaceValues;
23
+ userValues;
24
+ revisions;
25
+ nextSeq;
26
+ /** True while `transaction` is open — see its doc for why this refuses
27
+ * rather than nests. */
28
+ inTransaction = false;
29
+ constructor(seed = {}) {
30
+ this.definitions = [...(seed.definitions ?? [])];
31
+ this.globalValues = [...(seed.globalValues ?? [])];
32
+ this.workspaceValues = [...(seed.workspaceValues ?? [])];
33
+ this.userValues = [...(seed.userValues ?? [])];
34
+ this.revisions = [...(seed.revisions ?? [])];
35
+ this.nextSeq = (seed.revisions?.reduce((max, r) => Math.max(max, r.seq), 0) ?? 0) + 1;
36
+ }
37
+ async findActiveDefinition(required) {
38
+ // Matches active/alias/tombstone (the "current" row for this (ns,key,ws)
39
+ // slot — callers decide how to react to each status, e.g. settings.ts
40
+ // treats tombstone as read-absent but write-service.ts reports it
41
+ // distinctly as DEFINITION_TOMBSTONED). Deliberately excludes
42
+ // `deprecated`: those are stale prior-version rows superseded by a newer
43
+ // active row at the same (ns,key,ws) slot (the retype mechanism), so
44
+ // including them would make this lookup ambiguous.
45
+ return (this.definitions.find((d) => d.namespace === required.namespace &&
46
+ d.key === required.key &&
47
+ d.workspaceId === required.workspaceId &&
48
+ (d.status === "active" || d.status === "alias" || d.status === "tombstone")) ?? null);
49
+ }
50
+ async findDefinitionBySettingId(required) {
51
+ const candidates = this.definitions.filter((d) => d.settingId === required.settingId);
52
+ if (required.version != null) {
53
+ return candidates.find((d) => d.version === required.version) ?? null;
54
+ }
55
+ return (candidates.find((d) => d.status === "active") ??
56
+ candidates.sort((a, b) => b.version - a.version)[0] ??
57
+ null);
58
+ }
59
+ async listActiveDefinitions(required) {
60
+ return this.definitions.filter((d) => d.workspaceId === required.workspaceId && (d.status === "active" || d.status === "alias"));
61
+ }
62
+ async saveDefinition(record) {
63
+ const index = this.definitions.findIndex((d) => d.settingId === record.settingId && d.version === record.version);
64
+ if (index === -1) {
65
+ this.definitions.push(record);
66
+ return;
67
+ }
68
+ this.definitions[index] = record;
69
+ }
70
+ async getGlobalValue(settingId) {
71
+ return this.globalValues.find((v) => v.settingId === settingId) ?? null;
72
+ }
73
+ async getWorkspaceValue(required) {
74
+ return (this.workspaceValues.find((v) => v.workspaceId === required.workspaceId && v.settingId === required.settingId) ?? null);
75
+ }
76
+ async getUserValue(required) {
77
+ return (this.userValues.find((v) => v.workspaceId === required.workspaceId &&
78
+ v.principalId === required.principalId &&
79
+ v.settingId === required.settingId) ?? null);
80
+ }
81
+ async saveGlobalValue(record) {
82
+ const index = this.globalValues.findIndex((v) => v.settingId === record.settingId);
83
+ if (index === -1) {
84
+ this.globalValues.push(record);
85
+ return;
86
+ }
87
+ this.globalValues[index] = record;
88
+ }
89
+ async saveWorkspaceValue(record) {
90
+ const index = this.workspaceValues.findIndex((v) => v.workspaceId === record.workspaceId && v.settingId === record.settingId);
91
+ if (index === -1) {
92
+ this.workspaceValues.push(record);
93
+ return;
94
+ }
95
+ this.workspaceValues[index] = record;
96
+ }
97
+ async saveUserValue(record) {
98
+ const index = this.userValues.findIndex((v) => v.workspaceId === record.workspaceId &&
99
+ v.principalId === record.principalId &&
100
+ v.settingId === record.settingId);
101
+ if (index === -1) {
102
+ this.userValues.push(record);
103
+ return;
104
+ }
105
+ this.userValues[index] = record;
106
+ }
107
+ async listWorkspaceValues(required) {
108
+ return this.workspaceValues.filter((v) => v.workspaceId === required.workspaceId);
109
+ }
110
+ async listUserValues(required) {
111
+ return this.userValues.filter((v) => v.workspaceId === required.workspaceId && v.principalId === required.principalId);
112
+ }
113
+ async listUserValuesByWorkspace(required) {
114
+ return this.userValues.filter((v) => v.workspaceId === required.workspaceId);
115
+ }
116
+ async deleteWorkspaceValue(required) {
117
+ this.workspaceValues = this.workspaceValues.filter((v) => !(v.workspaceId === required.workspaceId && v.settingId === required.settingId));
118
+ }
119
+ async deleteUserValue(required) {
120
+ this.userValues = this.userValues.filter((v) => !(v.workspaceId === required.workspaceId &&
121
+ v.principalId === required.principalId &&
122
+ v.settingId === required.settingId));
123
+ }
124
+ async appendRevision(record) {
125
+ const seq = this.nextSeq++;
126
+ this.revisions.push({ ...record, seq });
127
+ return seq;
128
+ }
129
+ async listRevisions(required) {
130
+ return this.revisions.filter((r) => r.settingId === required.settingId).sort((a, b) => a.seq - b.seq);
131
+ }
132
+ async listRevisionsSince(required) {
133
+ return this.revisions
134
+ .filter((r) => r.seq > required.sinceSeq &&
135
+ // Mirrors the SQL predicate exactly — see the port's doc. `null` means
136
+ // platform-wide (every workspace resolves through it); anything else is one tenant's.
137
+ (r.workspaceId === null || r.workspaceId === required.workspaceId))
138
+ .sort((a, b) => a.seq - b.seq)
139
+ .slice(0, required.limit);
140
+ }
141
+ async maxRevisionSeq() {
142
+ return this.revisions.reduce((max, r) => (r.seq > max ? r.seq : max), 0);
143
+ }
144
+ /**
145
+ * Runs `fn` atomically: every mutation it makes is discarded if it throws.
146
+ *
147
+ * A shallow copy of each array is enough because no method here mutates a
148
+ * record in place — `save*` REPLACES the element at its index, `delete*`
149
+ * reassigns a filtered array, `appendRevision` pushes. Restoring the arrays
150
+ * therefore restores the rows too. If a future method starts editing a
151
+ * record object in place, this must deep-copy instead.
152
+ *
153
+ * `nextSeq` is rolled back with the rest, matching SQLite: `sqlite_sequence`
154
+ * is an ordinary table, so an AUTOINCREMENT counter bumped inside a
155
+ * transaction is undone by its ROLLBACK. A seq handed out by a failed
156
+ * transaction is safe to reuse precisely because that revision no longer
157
+ * exists.
158
+ *
159
+ * **Deliberately NOT reentrant**, matching a SQLite adapter's `transaction`,
160
+ * where `BEGIN IMMEDIATE` inside an open transaction is an error. Refusing
161
+ * loudly is the point: an instance-level flag cannot tell "nested inside my
162
+ * caller's transaction" from "an unrelated transaction that started while
163
+ * mine was awaiting". Silently joining a stranger's transaction — and
164
+ * inheriting its rollback — is a worse defect than refusing. A caller that
165
+ * needs several writes to commit together opens ONE transaction and passes
166
+ * `skipTransaction` to the inner writes, as `resetNamespace` does.
167
+ */
168
+ async transaction(fn) {
169
+ if (this.inTransaction) {
170
+ throw new Error("InMemorySettingsRepo.transaction is not reentrant, and cannot host a second overlapping transaction. " +
171
+ "Open one transaction and pass `skipTransaction` to the inner writes (see resetNamespace).");
172
+ }
173
+ const snapshot = {
174
+ definitions: [...this.definitions],
175
+ globalValues: [...this.globalValues],
176
+ workspaceValues: [...this.workspaceValues],
177
+ userValues: [...this.userValues],
178
+ revisions: [...this.revisions],
179
+ nextSeq: this.nextSeq,
180
+ };
181
+ this.inTransaction = true;
182
+ try {
183
+ return await fn();
184
+ }
185
+ catch (error) {
186
+ this.definitions = snapshot.definitions;
187
+ this.globalValues = snapshot.globalValues;
188
+ this.workspaceValues = snapshot.workspaceValues;
189
+ this.userValues = snapshot.userValues;
190
+ this.revisions = snapshot.revisions;
191
+ this.nextSeq = snapshot.nextSeq;
192
+ throw error;
193
+ }
194
+ finally {
195
+ this.inTransaction = false;
196
+ }
197
+ }
198
+ }
199
+ //# sourceMappingURL=repo.memory.js.map