@jini-ai/core 0.4.9 → 0.6.10
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 +16 -2
- package/README.md +5 -0
- package/dist/redact.d.ts +28 -0
- package/dist/redact.js +34 -0
- package/dist/settings/errors.d.ts +13 -0
- package/dist/settings/errors.js +15 -0
- package/dist/settings/index.d.ts +2 -2
- package/dist/settings/index.js +1 -1
- package/dist/settings/ports.d.ts +7 -1
- package/dist/settings/write-service.d.ts +23 -4
- package/dist/settings/write-service.js +38 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 22f0619: Export baseline agent-failure token patterns, credential-directory deny
|
|
8
|
+
segments, and local-owner proxy markers for reuse by consumers.
|
|
9
|
+
Preserve existing vocabulary and redaction behavior.
|
|
10
|
+
- 82ed2d3: Expose the React-free fetch-query contracts/cache at @jini-ai/core/fetch; UI keeps deprecated direct type re-exports. Build core before UI and publish before consumers adopt the subpath.
|
|
11
|
+
- 5a3de86: Add optional `expectedRevisionSeq` options to settings `set(required, options)` and
|
|
12
|
+
`clear(required, options)`. Compare the addressed scope's stored value revision under
|
|
13
|
+
the existing transaction's ledger lock, throwing `SettingsRevisionConflictError`
|
|
14
|
+
with expected and actual sequences on mismatch and rolling back the write and revision.
|
|
15
|
+
Revision 0 represents a missing row; cleared rows retain their revision. Omitting
|
|
16
|
+
the option preserves unconditional writes. Available through `@jini-ai/core/settings`
|
|
17
|
+
and the existing `@jini-ai/cms/settings` compatibility re-export.
|
|
18
|
+
|
|
3
19
|
## 0.4.9 — 2026-10-22
|
|
4
20
|
|
|
5
21
|
- 2026-10-22: Provider registry, ObjectStore contract with object_store registry and deferObjectStore, default-on batch tool eligibility, coverage.
|
|
@@ -56,7 +72,6 @@
|
|
|
56
72
|
- Move the domain-independent catalog/input/schema/risk/confirmation registration kit into the root entry, with its behavior contracts. Domain-specific authorization and denial errors remain outside the kernel.
|
|
57
73
|
- Use the existing named argument surface directly; primitive, Result and tool catalog types remain canonical in core.
|
|
58
74
|
|
|
59
|
-
|
|
60
75
|
### Shared kernel
|
|
61
76
|
|
|
62
77
|
- Add `./primitives` with canonical time, ID, JSON, HTTP, logger and result contracts,
|
|
@@ -80,7 +95,6 @@
|
|
|
80
95
|
- Origin diagnostics use `Pick<Logger, 'warn'>` rather than a duplicate logger interface.
|
|
81
96
|
The alias name `OriginValidationLoggerPort` remains available with the shared shape.
|
|
82
97
|
|
|
83
|
-
|
|
84
98
|
- Add transport-neutral gated mutations with required token generation/TTL and async
|
|
85
99
|
authorization, identity and token-store ports; preserve verified-plan handoff and
|
|
86
100
|
single-use redemption, with atomic explicit expiry and duplicate-issuance rejection.
|
package/README.md
CHANGED
|
@@ -44,6 +44,11 @@ peer used only by `@jini-ai/core/settings/express`; install it in hosts mounting
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
+
The root exports baseline security vocabulary: `AGENT_FAILURE_SECRET_SHAPE_RE`,
|
|
48
|
+
`CREDENTIAL_FILE_DENY_SEGMENTS`, `LOCAL_OWNER_PROXY_MARKER_PREFIXES`, and
|
|
49
|
+
`LOCAL_OWNER_PROXY_MARKER_HEADERS`. Consumers apply these in their existing failure-redaction,
|
|
50
|
+
guarded-file, and local-owner admission policies; the existing redactors are unchanged.
|
|
51
|
+
|
|
47
52
|
```ts
|
|
48
53
|
import { bindings, definePack, createDaemon, token, createToolRegistry, registerPackTools } from '@jini-ai/core';
|
|
49
54
|
|
package/dist/redact.d.ts
CHANGED
|
@@ -38,6 +38,34 @@ export interface SecretShapePattern {
|
|
|
38
38
|
}
|
|
39
39
|
export declare const PEM_PATTERN_NAME = "PEM private key block";
|
|
40
40
|
export declare const SECRET_SHAPE_PATTERNS: readonly SecretShapePattern[];
|
|
41
|
+
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
42
|
+
export declare const AGENT_FAILURE_SECRET_SHAPE_RE: RegExp;
|
|
43
|
+
/**
|
|
44
|
+
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
45
|
+
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
46
|
+
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
47
|
+
* credentials folder (`sites/**\/secrets/`, `apps/admin/**\/secrets/`, or anywhere else), it never
|
|
48
|
+
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
49
|
+
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
50
|
+
* not merely refused once named. Besides `secrets`, the set names the
|
|
51
|
+
* well-known credential-store directories a home-directory `custom` root can expose: `.aws`
|
|
52
|
+
* (`credentials`), `.docker` (`config.json` with registry auth), `.kube` (`config`), and
|
|
53
|
+
* `gcloud`/`gh` for `.config/gcloud/application_default_credentials.json` and
|
|
54
|
+
* `.config/gh/hosts.yml`. Denied by SEGMENT rather than by leaf filename on purpose: `config`,
|
|
55
|
+
* `config.json`, and `hosts.yml` are generic names that appear harmlessly elsewhere, so denying the
|
|
56
|
+
* directory is the narrower of the two mistakes.
|
|
57
|
+
*
|
|
58
|
+
* A host's install-wide site-key directory (2026-09-24) needs the same protection; the host appends it.
|
|
59
|
+
* Production stores it beside individual site folders on a durable volume; local/dev stores
|
|
60
|
+
* it below the home directory, reachable through an operator-set `custom` root. Its key backs
|
|
61
|
+
* site-key administration, webhook/newsletter signing secrets, and permission checks, so the
|
|
62
|
+
* agent must never read it from disk. Denying the whole directory segment, rather than only
|
|
63
|
+
* a generated key filename, also prevents `fs_list_files` from enumerating its contents — the
|
|
64
|
+
* same "never even advertise it" property `secrets` gets above.
|
|
65
|
+
*/
|
|
66
|
+
export declare const CREDENTIAL_FILE_DENY_SEGMENTS: readonly string[];
|
|
67
|
+
export declare const LOCAL_OWNER_PROXY_MARKER_PREFIXES: readonly string[];
|
|
68
|
+
export declare const LOCAL_OWNER_PROXY_MARKER_HEADERS: readonly string[];
|
|
41
69
|
export interface SecretRedactionPattern {
|
|
42
70
|
name: string;
|
|
43
71
|
regex: RegExp;
|
package/dist/redact.js
CHANGED
|
@@ -52,6 +52,40 @@ export const SECRET_SHAPE_PATTERNS = [
|
|
|
52
52
|
{ name: "Fastmail app password (fm2_)", pattern: /(?<![A-Za-z0-9+/=])fm2_[A-Za-z0-9+/=]{20,}/ },
|
|
53
53
|
{ name: PEM_PATTERN_NAME, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
54
54
|
];
|
|
55
|
+
/** Broad token shapes for agent-failure text; consumers retain their plain [REDACTED] pipeline. */
|
|
56
|
+
export const AGENT_FAILURE_SECRET_SHAPE_RE = /\b(?:AIza[\w-]+|AQ\.[\w.\/-]+|sk-[\w.-]+|gh[pousr]_[\w]+|github_pat_[\w]+|ya29\.[\w.-]+|eyJ[\w-]+\.[\w-]+(?:\.[\w-]+)?)/g;
|
|
57
|
+
/**
|
|
58
|
+
* Exact path SEGMENT names refused at any depth, matched case-insensitively against each segment
|
|
59
|
+
* of the caller-supplied path (never a substring — a directory named `not-secrets-actually` is not
|
|
60
|
+
* a match). `secrets` is the owner's explicit example: whatever a site or app tree names its
|
|
61
|
+
* credentials folder (`sites/**\/secrets/`, `apps/admin/**\/secrets/`, or anywhere else), it never
|
|
62
|
+
* becomes reachable through this domain. A denied segment is refused by the guarded reader
|
|
63
|
+
* AND skipped entirely by the `fs_list_files` walk — its contents are never even enumerated,
|
|
64
|
+
* not merely refused once named. Besides `secrets`, the set names the
|
|
65
|
+
* well-known credential-store directories a home-directory `custom` root can expose: `.aws`
|
|
66
|
+
* (`credentials`), `.docker` (`config.json` with registry auth), `.kube` (`config`), and
|
|
67
|
+
* `gcloud`/`gh` for `.config/gcloud/application_default_credentials.json` and
|
|
68
|
+
* `.config/gh/hosts.yml`. Denied by SEGMENT rather than by leaf filename on purpose: `config`,
|
|
69
|
+
* `config.json`, and `hosts.yml` are generic names that appear harmlessly elsewhere, so denying the
|
|
70
|
+
* directory is the narrower of the two mistakes.
|
|
71
|
+
*
|
|
72
|
+
* A host's install-wide site-key directory (2026-09-24) needs the same protection; the host appends it.
|
|
73
|
+
* Production stores it beside individual site folders on a durable volume; local/dev stores
|
|
74
|
+
* it below the home directory, reachable through an operator-set `custom` root. Its key backs
|
|
75
|
+
* site-key administration, webhook/newsletter signing secrets, and permission checks, so the
|
|
76
|
+
* agent must never read it from disk. Denying the whole directory segment, rather than only
|
|
77
|
+
* a generated key filename, also prevents `fs_list_files` from enumerating its contents — the
|
|
78
|
+
* same "never even advertise it" property `secrets` gets above.
|
|
79
|
+
*/
|
|
80
|
+
// Generic credential-store segments only. A host adds its own product-specific key directory
|
|
81
|
+
// (e.g. its install-wide site-key folder) when it builds its deny-list; core stays product-neutral.
|
|
82
|
+
export const CREDENTIAL_FILE_DENY_SEGMENTS = ["secrets", ".aws", ".docker", ".kube", "gcloud", "gh"];
|
|
83
|
+
// A local tunnel has a loopback socket and may rewrite Host; refuse marker presence, even empty.
|
|
84
|
+
export const LOCAL_OWNER_PROXY_MARKER_PREFIXES = ["x-forwarded-", "x-envoy-", "x-ms-"];
|
|
85
|
+
export const LOCAL_OWNER_PROXY_MARKER_HEADERS = [
|
|
86
|
+
"forwarded", "x-real-ip", "via", "cf-connecting-ip", "true-client-ip", "x-original-host",
|
|
87
|
+
"x-client-ip", "x-cluster-client-ip", "fastly-client-ip", "tailscale-user-login", "x-original-forwarded-for",
|
|
88
|
+
];
|
|
55
89
|
// Order matters: list specific rules before more general ones. Langfuse
|
|
56
90
|
// keys (`sk-lf-...`) would otherwise be eaten by the generic `sk-...`
|
|
57
91
|
// rule and labeled as a generic OpenAI-style key.
|
|
@@ -18,6 +18,19 @@ export declare class SecretNotSupportedError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
export declare class ValueValidationFailedError extends Error {
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A scoped value changed since the caller's snapshot; read again before retrying.
|
|
23
|
+
* Both sequences refer to the addressed value row, never the ledger's global watermark.
|
|
24
|
+
* @example if (error instanceof SettingsRevisionConflictError) await refreshSnapshot();
|
|
25
|
+
*/
|
|
26
|
+
export declare class SettingsRevisionConflictError extends Error {
|
|
27
|
+
readonly expectedRevisionSeq: number;
|
|
28
|
+
readonly actualRevisionSeq: number;
|
|
29
|
+
constructor(required: {
|
|
30
|
+
expectedRevisionSeq: number;
|
|
31
|
+
actualRevisionSeq: number;
|
|
32
|
+
}, optional?: ErrorOptions);
|
|
33
|
+
}
|
|
21
34
|
export declare class RenameRetypeConflictError extends Error {
|
|
22
35
|
}
|
|
23
36
|
export declare class AliasDepthExceededError extends Error {
|
package/dist/settings/errors.js
CHANGED
|
@@ -18,6 +18,21 @@ export class SecretNotSupportedError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
export class ValueValidationFailedError extends Error {
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A scoped value changed since the caller's snapshot; read again before retrying.
|
|
23
|
+
* Both sequences refer to the addressed value row, never the ledger's global watermark.
|
|
24
|
+
* @example if (error instanceof SettingsRevisionConflictError) await refreshSnapshot();
|
|
25
|
+
*/
|
|
26
|
+
export class SettingsRevisionConflictError extends Error {
|
|
27
|
+
expectedRevisionSeq;
|
|
28
|
+
actualRevisionSeq;
|
|
29
|
+
constructor(required, optional = {}) {
|
|
30
|
+
super(`Setting revision changed: expected ${required.expectedRevisionSeq}, found ${required.actualRevisionSeq}.`, optional);
|
|
31
|
+
this.name = "SettingsRevisionConflictError";
|
|
32
|
+
this.expectedRevisionSeq = required.expectedRevisionSeq;
|
|
33
|
+
this.actualRevisionSeq = required.actualRevisionSeq;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
export class RenameRetypeConflictError extends Error {
|
|
22
37
|
}
|
|
23
38
|
export class AliasDepthExceededError extends Error {
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* `SettingsRepoPort` this barrel exports.
|
|
12
12
|
*/
|
|
13
13
|
export { SCOPE_BIT, type SettingScope, type SettingOwnerKind, type DefinitionStatus, type ValueState, type RevisionEntityKind, type RevisionOp, type SettingValueSchema, type SettingDefinitionRecord, type SettingValueRecord, type SettingRevisionRecord, type SettingScopeContext, } from "./types.js";
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
14
|
+
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, SettingsRevisionConflictError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
15
|
export type { SettingsRepoPort } from "./ports.js";
|
|
16
16
|
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
17
17
|
export { type DefinitionInput, validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, type ResolvedSetting, getEffective, } from "./settings.js";
|
|
18
|
-
export { type AuthorizeFn, type SettingsWriteServiceDeps, deriveRequiredPermission, type RegisterDefinitionsRequired, registerDefinitions, type SetValueRequired, set, type ClearValueRequired, clear, type ResetNamespaceRequired, resetNamespace, type RenameDefinitionRequired, renameDefinition, type RetypeDefinitionRequired, retypeDefinition, type ReconcileDefinitionDefaultRequired, reconcileDefinitionDefault, type DeprecateDefinitionRequired, deprecateDefinition, type TombstoneDefinitionRequired, tombstoneDefinition, } from "./write-service.js";
|
|
18
|
+
export { type AuthorizeFn, type SettingsWriteServiceDeps, deriveRequiredPermission, type RegisterDefinitionsRequired, registerDefinitions, type SetValueRequired, type SettingsValueWriteOptions, set, type ClearValueRequired, clear, type ResetNamespaceRequired, resetNamespace, type RenameDefinitionRequired, renameDefinition, type RetypeDefinitionRequired, retypeDefinition, type ReconcileDefinitionDefaultRequired, reconcileDefinitionDefault, type DeprecateDefinitionRequired, deprecateDefinition, type TombstoneDefinitionRequired, tombstoneDefinition, } from "./write-service.js";
|
|
19
19
|
export { type PurgeServiceDeps, type PurgeTenantSettingsRequired, purgeTenantSettings, } from "./purge-service.js";
|
|
20
20
|
export { type ChangeFeedViewer, isRevisionVisibleTo, type ChangeFeedBatch, collectChangedNamespaces, } from "./change-feed.js";
|
|
21
21
|
export { type DefinitionOpRequestItem, type DefinitionOpContext, type DefinitionOpHandler, NON_REGISTER_DEFINITION_OP_NAMES, type NonRegisterDefinitionOp, parseNonRegisterDefinitionOp, NON_REGISTER_DEFINITION_OPS, } from "./definitions-dispatch.js";
|
package/dist/settings/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `SettingsRepoPort` this barrel exports.
|
|
12
12
|
*/
|
|
13
13
|
export { SCOPE_BIT, } from "./types.js";
|
|
14
|
-
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
14
|
+
export { DefinitionInvalidError, ScopeNotAllowedError, SecretNotSupportedError, ValueValidationFailedError, SettingsRevisionConflictError, RenameRetypeConflictError, AliasDepthExceededError, DefinitionTombstonedError, DefinitionNotFoundError, PurgeRequiredError, ForbiddenError, PrincipalNotFoundError, } from "./errors.js";
|
|
15
15
|
export { InMemorySettingsRepo } from "./repo.memory.js";
|
|
16
16
|
export { validateDefinitionInput, validateValueAgainstSchema, registerCoercer, invalidateDefinitionNamespaceCache, invalidateWorkspaceSettingsCache, resolveDefinitionRaw, resolveDefinition, getEffective, } from "./settings.js";
|
|
17
17
|
export { deriveRequiredPermission, registerDefinitions, set, clear, resetNamespace, renameDefinition, retypeDefinition, reconcileDefinitionDefault, deprecateDefinition, tombstoneDefinition, } from "./write-service.js";
|
package/dist/settings/ports.d.ts
CHANGED
|
@@ -68,7 +68,13 @@ export interface SettingsRepoPort {
|
|
|
68
68
|
principalId: UUID;
|
|
69
69
|
settingId: UUID;
|
|
70
70
|
}): Promise<void>;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Appends a revision and returns its assigned `seq` (used to stamp the paired value row).
|
|
73
|
+
* Inside a transaction, serialize revision writers until that enclosing transaction ends:
|
|
74
|
+
* `set`/`clear` compare the stored value's seq AFTER this call, so the lock must cover the
|
|
75
|
+
* comparison and value save too. SQLite's write transaction and Postgres's transaction-scoped
|
|
76
|
+
* ledger lock provide this; memory refuses overlapping transactions.
|
|
77
|
+
*/
|
|
72
78
|
appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
|
|
73
79
|
listRevisions(required: {
|
|
74
80
|
settingId: UUID;
|
|
@@ -110,8 +110,21 @@ export interface SetValueRequired {
|
|
|
110
110
|
requiredPermissionOverride?: string | undefined;
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
-
/**
|
|
114
|
-
export
|
|
113
|
+
/** Optional concurrency guard shared by `set` and `clear`. */
|
|
114
|
+
export interface SettingsValueWriteOptions {
|
|
115
|
+
/** Compare the addressed layer's stored seq, including cleared rows; 0 means no stored row.
|
|
116
|
+
* Omission preserves unconditional writes. Defaults, inherited values and definition revisions
|
|
117
|
+
* do not count. A mismatch throws SettingsRevisionConflictError and rolls back the write.
|
|
118
|
+
*/
|
|
119
|
+
readonly expectedRevisionSeq?: number | undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Authorize -> validate -> value + revision in one transaction. An optional expectedRevisionSeq
|
|
123
|
+
* refuses a stale scoped value and rolls back its revision; omission keeps unconditional writes.
|
|
124
|
+
* @example await set({ deps, input }, { expectedRevisionSeq: snapshotSeq });
|
|
125
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
126
|
+
*/
|
|
127
|
+
export declare function set(required: SetValueRequired, optional?: SettingsValueWriteOptions): Promise<{
|
|
115
128
|
value: JsonValue;
|
|
116
129
|
revisionSeq: number;
|
|
117
130
|
}>;
|
|
@@ -135,8 +148,14 @@ export interface ClearValueRequired {
|
|
|
135
148
|
authWorkspaceId?: UUID | undefined;
|
|
136
149
|
};
|
|
137
150
|
}
|
|
138
|
-
/**
|
|
139
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Authorize -> cleared value + revision in one transaction; the optional concurrency guard has
|
|
153
|
+
* the same semantics as `set`. With skipTransaction, the enclosing caller must propagate errors
|
|
154
|
+
* so its transaction rolls back the appended revision on conflict.
|
|
155
|
+
* @example await clear({ deps, input }, { expectedRevisionSeq: snapshotSeq });
|
|
156
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
157
|
+
*/
|
|
158
|
+
export declare function clear(required: ClearValueRequired, optional?: SettingsValueWriteOptions): Promise<{
|
|
140
159
|
revisionSeq: number;
|
|
141
160
|
}>;
|
|
142
161
|
export interface ResetNamespaceRequired {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nowIso as kernelNowIso } from "../primitives/index.js";
|
|
2
|
-
import { AliasDepthExceededError, DefinitionInvalidError, DefinitionNotFoundError, DefinitionTombstonedError, ForbiddenError, PrincipalNotFoundError, RenameRetypeConflictError, ScopeNotAllowedError, ValueValidationFailedError, } from "./errors.js";
|
|
2
|
+
import { AliasDepthExceededError, DefinitionInvalidError, DefinitionNotFoundError, DefinitionTombstonedError, ForbiddenError, PrincipalNotFoundError, RenameRetypeConflictError, ScopeNotAllowedError, SettingsRevisionConflictError, ValueValidationFailedError, } from "./errors.js";
|
|
3
3
|
import { invalidateDefinitionNamespaceCache, resolveDefinitionRaw, validateDefinitionInput, validateValueAgainstSchema, } from "./settings.js";
|
|
4
4
|
import { SCOPE_BIT } from "./types.js";
|
|
5
5
|
/**
|
|
@@ -148,8 +148,33 @@ async function assertTargetPrincipalInWorkspace(deps, input) {
|
|
|
148
148
|
throw new PrincipalNotFoundError(`principal '${input.principalId}' was not found in workspace '${input.workspaceId}'`, input.principalId, input.workspaceId);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Check the addressed value after appendRevision has taken the ledger lock. On Postgres, a read
|
|
153
|
+
* before that lock can see the same old seq in two READ COMMITTED transactions and lose an update.
|
|
154
|
+
* The caller's enclosing transaction rolls the appended revision back on conflict.
|
|
155
|
+
* @complexity At most one scoped value read; no reads when the option is omitted.
|
|
156
|
+
*/
|
|
157
|
+
async function assertExpectedRevision(required, optional = {}) {
|
|
158
|
+
if (optional.expectedRevisionSeq === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const { repo, settingId, input } = required;
|
|
161
|
+
const current = input.scope === "global"
|
|
162
|
+
? await repo.getGlobalValue(settingId)
|
|
163
|
+
: input.scope === "workspace"
|
|
164
|
+
? await repo.getWorkspaceValue({ workspaceId: input.workspaceId, settingId })
|
|
165
|
+
: await repo.getUserValue({ workspaceId: input.workspaceId, principalId: input.principalId ?? input.callerPrincipalId, settingId });
|
|
166
|
+
const actualRevisionSeq = current?.seq ?? 0;
|
|
167
|
+
if (actualRevisionSeq !== optional.expectedRevisionSeq) {
|
|
168
|
+
throw new SettingsRevisionConflictError({ expectedRevisionSeq: optional.expectedRevisionSeq, actualRevisionSeq });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Authorize -> validate -> value + revision in one transaction. An optional expectedRevisionSeq
|
|
173
|
+
* refuses a stale scoped value and rolls back its revision; omission keeps unconditional writes.
|
|
174
|
+
* @example await set({ deps, input }, { expectedRevisionSeq: snapshotSeq });
|
|
175
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
176
|
+
*/
|
|
177
|
+
export async function set(required, optional = {}) {
|
|
153
178
|
const { deps, input } = required;
|
|
154
179
|
const permission = input.requiredPermissionOverride ??
|
|
155
180
|
deriveRequiredPermission({
|
|
@@ -190,6 +215,7 @@ export async function set(required) {
|
|
|
190
215
|
changeSetId: null,
|
|
191
216
|
createdAt: now,
|
|
192
217
|
});
|
|
218
|
+
await assertExpectedRevision({ repo: deps.repo, settingId: definition.settingId, input }, optional);
|
|
193
219
|
const base = {
|
|
194
220
|
settingId: definition.settingId,
|
|
195
221
|
scope: input.scope,
|
|
@@ -222,8 +248,14 @@ export async function set(required) {
|
|
|
222
248
|
});
|
|
223
249
|
return result;
|
|
224
250
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Authorize -> cleared value + revision in one transaction; the optional concurrency guard has
|
|
253
|
+
* the same semantics as `set`. With skipTransaction, the enclosing caller must propagate errors
|
|
254
|
+
* so its transaction rolls back the appended revision on conflict.
|
|
255
|
+
* @example await clear({ deps, input }, { expectedRevisionSeq: snapshotSeq });
|
|
256
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
257
|
+
*/
|
|
258
|
+
export async function clear(required, optional = {}) {
|
|
227
259
|
const { deps, input } = required;
|
|
228
260
|
if (!input.skipAuthorize) {
|
|
229
261
|
const permission = deriveRequiredPermission({
|
|
@@ -262,6 +294,7 @@ export async function clear(required) {
|
|
|
262
294
|
changeSetId: null,
|
|
263
295
|
createdAt: now,
|
|
264
296
|
});
|
|
297
|
+
await assertExpectedRevision({ repo: deps.repo, settingId: definition.settingId, input }, optional);
|
|
265
298
|
const base = {
|
|
266
299
|
settingId: definition.settingId,
|
|
267
300
|
scope: input.scope,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|