@sentropic/artifacts 0.2.3 → 0.2.4

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/README.md CHANGED
@@ -76,7 +76,69 @@ Reconcile current authority independently of the restored ACL snapshot. Fresh
76
76
  authorized sessions create fresh flows; revoked readers stay denied. Rollback
77
77
  disables activation while retaining the additive schema and accepted artifacts.
78
78
 
79
- Package conformance lives in `tests/postgres-activation.test.ts` and
79
+ ### Importable host conformance (UP-T-2, 0.2.4)
80
+
81
+ The packed package exports the framework-independent host suite and its stable
82
+ vector IDs. Install the SQL asset through your test migration executor first:
83
+
84
+ ```ts
85
+ import assert from 'node:assert/strict';
86
+ import {
87
+ POSTGRES_ACTIVATION_VECTOR_IDS, runPostgresActivationConformance,
88
+ } from '@sentropic/artifacts/adapters/postgres-activation/vectors';
89
+
90
+ assert.equal(process.env.ACTIVATION_DISPOSABLE, '1');
91
+ const { rows: [{ databaseName }] } = await pool.query('SELECT current_database() AS "databaseName"');
92
+ assert.match(databaseName, /^test[_-][a-z0-9_-]+$/i); // Check before any schema setup/reset.
93
+ const passed = await runPostgresActivationConformance({
94
+ disposable: { confirmed: true, databaseName },
95
+ createStore: namespace => createPostgresActivationStore({ pool, namespace }),
96
+ inspectTicket, // Host query returning persisted issuedAt/expiresAt and current DB now, in milliseconds.
97
+ createStoreWithCommitAckLoss, // Host composition with acknowledgement loss AFTER real consume COMMIT.
98
+ wait: async (ms, client) => { await (client ?? pool).query('SELECT pg_sleep($1)', [ms / 1000]); },
99
+ onPass: id => console.log('activation vector passed:', id),
100
+ });
101
+ assert.deepEqual(passed, POSTGRES_ACTIVATION_VECTOR_IDS); // 16 vectors
102
+ ```
103
+
104
+ Supply your host's store composition in `createStore`, using the supplied namespace
105
+ and the same disposable PostgreSQL database/executor for every instance. Replica
106
+ instances must share durable state. `wait` uses database time and the supplied
107
+ leased client during authority checks, including when the pool has one connection.
108
+ `inspectTicket(namespace, ticketId)` reads `issued_ms`, `expires_ms` and
109
+ `floor(extract(epoch FROM clock_timestamp()) * 1000)` through that same executor.
110
+ `createStoreWithCommitAckLoss(namespace)` wraps the host's leased client: execute
111
+ COMMIT, then throw before returning its acknowledgement. Seed with the normal
112
+ store; inject only into the store returned by this hook. Both hooks are optional
113
+ in the interface for additive compatibility; their vectors always run and fail
114
+ if either hook is absent. A complete host fixture is in
115
+ `packages/cluster-mesh/tests/packaging/activation-vectors.spec.ts`.
116
+ Run in an isolated process: the clock-skew vector temporarily replaces `Date.now`
117
+ by +/-120 seconds and restores it even on failure. TypeScript hosts install
118
+ `@types/pg` alongside `pg` in their consumer project.
119
+ The runner throws with the failing vector ID and original cause; it returns all
120
+ 16 IDs only after every assertion passes. It uses fresh namespaces and invalidates
121
+ and cleans its records after each vector. Namespace rows remain in the disposable
122
+ database. The runner refuses without explicit disposable confirmation and an
123
+ actual database name starting `test_` or `test-`. Check `current_database()` on the
124
+ host executor before installing/resetting the schema. Never use a serving database.
125
+
126
+ The vectors cover namespace isolation, immutable records, all nine exact binding
127
+ comparisons, wrong IDs, denial/outage preservation, single use, concurrent replicas,
128
+ expiry without cleanup and after authority waits, bounded cleanup, invalidation
129
+ and fresh recovery, the 60-second TTL cap, flow/ticket association, DB-time-only
130
+ issuance/expiry under app clock skew, and `ActivationCommitUnknownError` without
131
+ a grant followed by replay denial. This adds
132
+ only a testing export in 0.2.4; adapter behavior and SQL bytes are unchanged.
133
+
134
+ `make -f packages/cluster-mesh/packaging.mk test-lazy-package-activation ENV=test-artifacts-upt2`
135
+ packs and installs the candidate in a clean consumer, verifies the SQL hash, and
136
+ runs all 16 vectors on that consumer's PostgreSQL executor (pool max 1 and 4).
137
+ It requires explicit disposable configuration and checks the JSON result for
138
+ nine passing tests and zero skipped tests; missing CI wiring fails. Follow with
139
+ `make down ENV=test-artifacts-upt2` (use the shared Docker lock for both commands).
140
+
141
+ Additional fault/process conformance lives in `tests/postgres-activation.test.ts` and
80
142
  `tests/postgres-activation.integration.test.ts`, with the independent process
81
143
  witness `tests/fixtures/activation-process.mjs`. Run through
82
144
  `make test-artifacts` and `make test-artifacts-integration` on an isolated test
@@ -0,0 +1,31 @@
1
+ import type { PoolClient } from 'pg';
2
+ import { type PostgresActivationStore } from './postgres-activation.js';
3
+ export declare const POSTGRES_ACTIVATION_VECTOR_IDS: readonly ["namespace-isolation", "immutable-records", "binding-mismatch", "wrong-ids", "authority-denial", "authority-outage", "single-use", "concurrent-consume", "expiry-without-cleanup", "expiry-after-authority", "bounded-cleanup", "invalidation-recovery", "ttl-cap", "flow-association", "db-time-skew", "commit-unknown"];
4
+ export type PostgresActivationVectorId = typeof POSTGRES_ACTIVATION_VECTOR_IDS[number];
5
+ export interface PostgresActivationConformanceHost {
6
+ /** Explicit opt-in; databaseName must be the executor's actual current_database(), starting test_ or test-. */
7
+ disposable?: {
8
+ confirmed: true;
9
+ databaseName: string;
10
+ };
11
+ /** Same disposable DB/executor for every namespace and replica; schema installed by the host. */
12
+ createStore(namespace: string): PostgresActivationStore;
13
+ /** Wait on DB time; use the supplied leased client during authority checks (supports max:1). */
14
+ wait(milliseconds: number, client?: PoolClient): Promise<void>;
15
+ /** Read persisted issuance/expiry and current DB milliseconds through the host executor. */
16
+ inspectTicket?(namespace: string, ticketId: string): Promise<{
17
+ issuedAt: number;
18
+ expiresAt: number;
19
+ now: number;
20
+ }>;
21
+ /** Same host composition, but throw AFTER the consume COMMIT succeeds, losing its acknowledgement. */
22
+ createStoreWithCommitAckLoss?(namespace: string): PostgresActivationStore;
23
+ onPass?(id: PostgresActivationVectorId): void;
24
+ }
25
+ /** Test-only, framework-independent host conformance. Throws on the first failing vector.
26
+ * Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
27
+ * Process crashes and restore snapshots remain package integration tests.
28
+ * Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
29
+ * Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
30
+ */
31
+ export declare function runPostgresActivationConformance(host: PostgresActivationConformanceHost): Promise<PostgresActivationVectorId[]>;
@@ -0,0 +1,178 @@
1
+ import assert from 'node:assert/strict';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { ActivationCommitUnknownError } from './postgres-activation.js';
4
+ export const POSTGRES_ACTIVATION_VECTOR_IDS = Object.freeze([
5
+ 'namespace-isolation', 'immutable-records', 'binding-mismatch', 'wrong-ids',
6
+ 'authority-denial', 'authority-outage', 'single-use', 'concurrent-consume',
7
+ 'expiry-without-cleanup', 'expiry-after-authority', 'bounded-cleanup',
8
+ 'invalidation-recovery', 'ttl-cap', 'flow-association',
9
+ 'db-time-skew', 'commit-unknown',
10
+ ]);
11
+ /** Test-only, framework-independent host conformance. Throws on the first failing vector.
12
+ * Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
13
+ * Process crashes and restore snapshots remain package integration tests.
14
+ * Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
15
+ * Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
16
+ */
17
+ export async function runPostgresActivationConformance(host) {
18
+ assert.ok(host.disposable?.confirmed === true, 'Explicit disposable database confirmation is required');
19
+ assert.match(host.disposable.databaseName, /^test[_-][a-z0-9_-]+$/i, 'A test database name is required');
20
+ const passed = [];
21
+ for (const id of POSTGRES_ACTIVATION_VECTOR_IDS) {
22
+ const namespace = `upt2:${randomUUID()}`;
23
+ const store = host.createStore(namespace);
24
+ const foreign = host.createStore(`${namespace}:other`);
25
+ const expected = { subjectId: 'alice', artifactId: 'a', releaseId: 'r', channel: 'stable',
26
+ nonceDigest: 'ab'.repeat(32), sessionId: 'session', profileDigest: 'cd'.repeat(32),
27
+ policyRevision: 'p1', targetOrigin: 'https://activation.example.test' };
28
+ async function seed(ttl = 50_000, target = store) {
29
+ assert.ok(target.tickets.issue, 'DB-time ticket issuance is required');
30
+ const ticket = await target.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl);
31
+ const flow = { ...expected, ticketId: ticket.ticketId, flowId: randomUUID(), expiresAt: ticket.expiresAt };
32
+ await target.flows.put(flow);
33
+ return { ticket, flow, input: { ticketId: ticket.ticketId, flowId: flow.flowId, expected } };
34
+ }
35
+ try {
36
+ const { ticket, flow, input } = await seed();
37
+ const redeem = () => store.consume(input, async () => true);
38
+ const preserved = async () => {
39
+ assert.deepEqual(await store.tickets.peek(ticket.ticketId), ticket);
40
+ assert.deepEqual(await store.flows.peek(flow.flowId), flow);
41
+ };
42
+ if (id === 'namespace-isolation') {
43
+ assert.equal(await foreign.tickets.peek(ticket.ticketId), undefined);
44
+ assert.equal(await foreign.flows.peek(flow.flowId), undefined);
45
+ assert.equal(await foreign.consume(input, async () => true), undefined);
46
+ await foreign.tickets.remove(ticket.ticketId);
47
+ await preserved();
48
+ }
49
+ else if (id === 'immutable-records') {
50
+ await assert.rejects(store.tickets.put({ ...ticket, subjectId: 'mallory' }));
51
+ await assert.rejects(store.flows.put({ ...flow, sessionId: 'other' }));
52
+ await preserved();
53
+ }
54
+ else if (id === 'binding-mismatch') {
55
+ for (const key of Object.keys(expected)) {
56
+ const mismatch = { ...expected, [key]: key === 'channel' ? 'pinned' : 'different' };
57
+ assert.equal(await store.consume({ ...input, expected: mismatch }, async () => true), undefined, key);
58
+ }
59
+ await preserved();
60
+ assert.deepEqual(await redeem(), ticket);
61
+ }
62
+ else if (id === 'wrong-ids') {
63
+ for (const key of ['ticketId', 'flowId']) {
64
+ assert.equal(await store.consume({ ...input, [key]: randomUUID() }, async () => true), undefined);
65
+ }
66
+ await preserved();
67
+ }
68
+ else if (id === 'authority-denial') {
69
+ assert.equal(await store.consume(input, async () => false), undefined);
70
+ await preserved();
71
+ }
72
+ else if (id === 'authority-outage') {
73
+ const outage = new Error('conformance authority unavailable');
74
+ await assert.rejects(store.consume(input, async () => { throw outage; }), error => error === outage);
75
+ await preserved();
76
+ }
77
+ else if (id === 'single-use') {
78
+ assert.deepEqual(await redeem(), ticket);
79
+ assert.equal(await redeem(), undefined);
80
+ assert.equal(await store.flows.peek(flow.flowId), undefined);
81
+ }
82
+ else if (id === 'concurrent-consume') {
83
+ const replica = host.createStore(namespace);
84
+ const results = await Promise.all([redeem(), replica.consume(input, async () => true)]);
85
+ assert.deepEqual(results.filter(Boolean), [ticket]);
86
+ }
87
+ else if (id === 'expiry-without-cleanup' || id === 'expiry-after-authority') {
88
+ const expired = await seed(1000);
89
+ if (id === 'expiry-without-cleanup')
90
+ await host.wait(1200);
91
+ assert.equal(await store.consume(expired.input, async ({ client }) => {
92
+ await host.wait(1200, client);
93
+ return true;
94
+ }), undefined);
95
+ assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
96
+ assert.equal(await store.flows.peek(expired.flow.flowId), undefined);
97
+ }
98
+ else if (id === 'bounded-cleanup') {
99
+ for (let i = 0; i < 3; i++)
100
+ await seed(1000);
101
+ const other = await seed(50_000, foreign);
102
+ await host.wait(1200);
103
+ let total = 0;
104
+ for (let i = 0; i < 4; i++) {
105
+ const removed = await store.cleanup(2);
106
+ const count = removed.flows + removed.tickets;
107
+ assert.ok(count >= 0 && count <= 2);
108
+ total += count;
109
+ }
110
+ assert.equal(total, 6);
111
+ await preserved();
112
+ assert.deepEqual(await foreign.tickets.peek(other.ticket.ticketId), other.ticket);
113
+ }
114
+ else if (id === 'invalidation-recovery') {
115
+ await store.invalidate();
116
+ const replica = host.createStore(namespace);
117
+ assert.equal(await replica.consume(input, async () => true), undefined);
118
+ assert.equal(await replica.tickets.peek(ticket.ticketId), undefined);
119
+ assert.equal(await replica.flows.peek(flow.flowId), undefined);
120
+ const fresh = await seed();
121
+ assert.deepEqual(await replica.consume(fresh.input, async () => true), fresh.ticket);
122
+ }
123
+ else if (id === 'ttl-cap') {
124
+ for (const ttl of [0, 60_001]) {
125
+ await assert.rejects(store.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl));
126
+ }
127
+ await seed(60_000);
128
+ }
129
+ else if (id === 'flow-association') {
130
+ await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), subjectId: 'mallory' }));
131
+ await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), expiresAt: ticket.expiresAt + 1 }));
132
+ }
133
+ else if (id === 'db-time-skew') {
134
+ assert.ok(host.inspectTicket, 'inspectTicket hook is required for db-time-skew');
135
+ const originalNow = Date.now;
136
+ try {
137
+ for (const skew of [-120_000, 120_000]) {
138
+ Date.now = () => originalNow() + skew;
139
+ const issued = await seed(60_000);
140
+ const db = await host.inspectTicket(namespace, issued.ticket.ticketId);
141
+ assert.equal(db.expiresAt - db.issuedAt, 60_000);
142
+ assert.equal(issued.ticket.expiresAt, db.expiresAt);
143
+ assert.ok(db.expiresAt - db.now > 59_000 && db.expiresAt - db.now <= 60_000);
144
+ assert.deepEqual(await store.consume(issued.input, async () => true), issued.ticket);
145
+ const expired = await seed(50);
146
+ await host.wait(80);
147
+ assert.equal(await store.consume(expired.input, async () => true), undefined);
148
+ assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
149
+ }
150
+ }
151
+ finally {
152
+ Date.now = originalNow;
153
+ }
154
+ }
155
+ else if (id === 'commit-unknown') {
156
+ assert.ok(host.createStoreWithCommitAckLoss, 'createStoreWithCommitAckLoss hook is required for commit-unknown');
157
+ const uncertain = host.createStoreWithCommitAckLoss(namespace);
158
+ const grants = [];
159
+ await assert.rejects(uncertain.consume(input, async () => true).then(result => grants.push(result)), error => error instanceof ActivationCommitUnknownError);
160
+ assert.deepEqual(grants, []);
161
+ assert.equal(await host.createStore(namespace).consume(input, async () => true), undefined);
162
+ assert.equal(await store.flows.peek(flow.flowId), undefined);
163
+ }
164
+ }
165
+ catch (cause) {
166
+ throw new Error(`UP-T conformance failed: ${id}`, { cause });
167
+ }
168
+ finally {
169
+ for (const target of [store, foreign]) {
170
+ await target.invalidate();
171
+ await target.cleanup(1000);
172
+ }
173
+ }
174
+ passed.push(id);
175
+ host.onPass?.(id);
176
+ }
177
+ return passed;
178
+ }
@@ -10,7 +10,7 @@ export { createRadarKeyDerivation } from './server/command-identity.js';
10
10
  export type { ArtifactKeyDerivationBinding } from './server/command-identity.js';
11
11
  export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
12
12
  export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
13
- export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.3";
13
+ export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.4";
14
14
  export declare const ARTIFACTS_TOOL_NAMES: readonly ["artifacts_get_template", "artifacts_get_api_guide", "artifacts_ensure", "artifacts_publish", "artifacts_create_upload", "artifacts_commit_upload", "artifacts_list", "artifacts_get", "artifacts_get_source", "artifacts_fork", "artifacts_set_access", "artifacts_promote", "artifacts_renew", "artifacts_delete", "artifacts_restore"];
15
15
  export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
16
16
  export declare const ARTIFACTS_TOOL_SPECS: {
package/dist/mcp-core.js CHANGED
@@ -6,7 +6,7 @@ export * from './profile/index.js';
6
6
  export { createRadarKeyDerivation } from './server/command-identity.js';
7
7
  export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
8
8
  export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
9
- export const ARTIFACTS_PROVIDER_VERSION = '0.2.3';
9
+ export const ARTIFACTS_PROVIDER_VERSION = '0.2.4';
10
10
  export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
11
11
  export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
12
12
  description: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Generic configurable artifacts MCP and artifact service: StorageScope/Artifact/Binding/Release model, canonical manifests, object authorization, idempotent intents (spec-v8 §9).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,10 @@
45
45
  "types": "./dist/adapters/postgres-activation.d.ts",
46
46
  "import": "./dist/adapters/postgres-activation.js"
47
47
  },
48
+ "./adapters/postgres-activation/vectors": {
49
+ "types": "./dist/adapters/postgres-activation-vectors.d.ts",
50
+ "import": "./dist/adapters/postgres-activation-vectors.js"
51
+ },
48
52
  "./sql/*": "./sql/*",
49
53
  "./migrations/*": "./migrations/*",
50
54
  "./server/ports": {