@sentropic/artifacts 0.2.4 → 0.2.6

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 (50) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +51 -3
  3. package/dist/adapters/postgres-management.d.ts +4 -0
  4. package/dist/adapters/postgres-management.js +36 -0
  5. package/dist/adapters/postgres.js +30 -8
  6. package/dist/http.d.ts +11 -1
  7. package/dist/http.js +272 -38
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +3 -0
  10. package/dist/mcp-core.d.ts +3 -3
  11. package/dist/mcp-core.js +29 -10
  12. package/dist/profile/contracts.d.ts +15 -5
  13. package/dist/profile/contracts.js +9 -6
  14. package/dist/profile/index.js +39 -4
  15. package/dist/profile/radar.js +4 -1
  16. package/dist/profile/schemas.js +41 -8
  17. package/dist/profile/validation.d.ts +3 -1
  18. package/dist/profile/validation.js +36 -13
  19. package/dist/server/access.d.ts +14 -0
  20. package/dist/server/command-identity.d.ts +6 -3
  21. package/dist/server/command-identity.js +8 -8
  22. package/dist/server/commit/coordinator.js +13 -10
  23. package/dist/server/commit/types.d.ts +4 -0
  24. package/dist/server/edits.d.ts +17 -0
  25. package/dist/server/edits.js +43 -0
  26. package/dist/server/failures.d.ts +34 -2
  27. package/dist/server/failures.js +17 -3
  28. package/dist/server/lifecycle.d.ts +4 -0
  29. package/dist/server/lifecycle.js +15 -0
  30. package/dist/server/management.d.ts +46 -0
  31. package/dist/server/management.js +86 -0
  32. package/dist/server/outcome-projection.d.ts +2 -2
  33. package/dist/server/outcome-projection.js +25 -7
  34. package/dist/server/publication.d.ts +22 -11
  35. package/dist/server/publication.js +56 -7
  36. package/dist/server/result-schema.d.ts +14 -0
  37. package/dist/server/result-schema.js +35 -0
  38. package/dist/server/service.d.ts +2 -0
  39. package/dist/server/service.js +2 -0
  40. package/dist/server/source-read.d.ts +38 -0
  41. package/dist/server/source-read.js +68 -0
  42. package/dist/server/summary.d.ts +40 -0
  43. package/dist/server/summary.js +18 -0
  44. package/dist/server/templates.js +24 -3
  45. package/dist/server/versions.d.ts +29 -0
  46. package/dist/server/versions.js +39 -0
  47. package/docs/migration-l6.md +8 -1
  48. package/migrations/0004_terminal_controls.sql +47 -0
  49. package/migrations/0005_metadata.sql +14 -0
  50. package/package.json +15 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 0.2.6
4
+
5
+ - Integrate part 1 with the source reads, sharing, terminal deletion, masking and
6
+ summaries published in 0.2.5. Apply `0005_metadata.sql` after 0004; it adds
7
+ editorial metadata and nullable release authors/lineage without backfilling history.
8
+ - Add version history and revert as a new release on the same stable URL, preserving
9
+ current metadata and ACLs. The v2 profile uses `targetReleaseId` and paginated history.
10
+ - Add full-bundle or exactly-once text edits, metadata on ensure/publish, and
11
+ transactional release CAS, metadata, references, quota and receipts.
12
+ - Separate trusted `profileRevisionId`, `requireExpectedCurrentReleaseId` and
13
+ `resultEnvelopeVersion`. Compatible bindings keep root results; strict v2 retains
14
+ commit state, replay status and typed failures through mcp-platform 0.4.1.
15
+ - Replay historical receipts before enforcing new preconditions or preparing edits.
16
+ Missing strict preconditions use the R2 `precondition_required` message and details.
17
+
18
+ IMMO activates part 1 only after its qualified input and result switchovers.
19
+ Artifacts 0.2.6 stays inside the existing cluster-mesh `>=0.2.0 <0.3.0` range;
20
+ the API consumes the workspace package and retains its pinned APP-P1 profile.
21
+
22
+ ## 0.2.5
23
+
24
+ - D1: Read verified author manifests and file bytes for the current or a selected
25
+ release, with path selection, source authorization, bounded snapshots and
26
+ separate provenance. Reject purged releases and mismatched record digests.
27
+ - D2: Grant or revoke named read access and delete artifacts terminally with
28
+ host-approved confirmation, transactional receipts and idempotent replay.
29
+ Mask artifacts outside the caller's visibility as `not_found`.
30
+ - D4: Enrich get/list results with binding names, dates, current release IDs,
31
+ digests, stable/pinned URLs, permissions and available optional metadata.
32
+
33
+ Apply `0004_terminal_controls.sql` after migrations 0001–0003 before enabling
34
+ the management capabilities.
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @sentropic/artifacts — generic artifact contracts (L-5a)
2
2
 
3
+ Publication profiles can set `requireExpectedCurrentReleaseId: true` to require an
4
+ explicit UUID or `null` on publish/revert. Strict profiles reject writers without
5
+ CAS. Omission remains supported in generic profiles. Replay the same caller key
6
+ after an unknown commit outcome; changing files, edits, metadata or the precondition
7
+ requires a new key. The `artifacts-outcome/v2` MCP envelope requires
8
+ `@sentropic/mcp-platform >=0.4.1` and preserves commit state and typed error details.
9
+
3
10
  ## Shared PostgreSQL activation (UP-T, 0.2.3)
4
11
 
5
12
  W1.c/W3.a and Radar S1/A6 use the same imports:
@@ -314,7 +321,8 @@ The installed package includes SQL resources exported under
314
321
  ```js
315
322
  import { readFile } from 'node:fs/promises';
316
323
 
317
- const migrations = ['0001_artifacts.sql', '0002_lifecycle.sql'];
324
+ const migrations = ['0001_artifacts.sql', '0002_lifecycle.sql', '0003_coordinator.sql',
325
+ '0004_terminal_controls.sql', '0005_metadata.sql'];
318
326
  for (const name of migrations) {
319
327
  const sql = await readFile(new URL(import.meta.resolve(
320
328
  `@sentropic/artifacts/migrations/${name}`,
@@ -324,8 +332,10 @@ for (const name of migrations) {
324
332
  ```
325
333
 
326
334
  Apply `0001_artifacts.sql` first to create the domain schema, then
327
- `0002_lifecycle.sql` to add lifecycle, sharing, sources and GC support, before
328
- using the Postgres store. The host owns migration credentials, serialization,
335
+ `0002_lifecycle.sql` to add lifecycle, sharing, sources and GC support, then
336
+ `0003_coordinator.sql`, `0004_terminal_controls.sql`, and `0005_metadata.sql`
337
+ (artifact display metadata and nullable release lineage),
338
+ before using the Postgres store. The host owns migration credentials, serialization,
329
339
  transactions and the migration journal; execute each version through that
330
340
  runner rather than running this read loop at application startup.
331
341
 
@@ -417,3 +427,41 @@ publication, use the current package version and this bootstrap sequence
417
427
  3. After that, merges to `main` touching the artifacts package publish
418
428
  through the `publish-artifacts` job (`make publish-artifacts`, OIDC +
419
429
  provenance), gated by `validate-artifacts`.
430
+
431
+ ## Source and management capabilities
432
+
433
+ Version 0.3.0 introduces D1, D2 and D4; see [CHANGELOG.md](./CHANGELOG.md).
434
+
435
+ Hosts may explicitly select `artifacts_get_source`, `artifacts_set_access`, and
436
+ `artifacts_delete` in their compiled profile. Configure `get_source` with empty
437
+ limits/defaults, and `set_access`/`delete` with `idempotencyKeyMaxUnits: 256` and
438
+ empty defaults. Existing profile selections remain explicit.
439
+
440
+ `POST /get_source` accepts `artifactId`, optional `releaseId`, and optional unique
441
+ `paths` (1–32 entries). Bind `publication.getRelease` and `sources.getSource` on
442
+ the HTTP app. It returns the canonical manifest, verified author file bytes,
443
+ and separate nullable `sourceRef`; readers need the distinct source permission.
444
+ The snapshot is limited to 32 files and 262144 decoded bytes. Hosts must qualify
445
+ a 2 MiB serialized MCP result budget before selecting this capability.
446
+
447
+ `POST /set_access` grants or revokes a named principal's read access, requires a
448
+ caller `idempotencyKey`, and keeps the artifact private. Bind
449
+ `isApprovedAccount` to the host's principal policy. `POST /delete` also requires
450
+ `expectedCurrentReleaseId` (nullable) and a trusted `management.confirmation`
451
+ port. Its `request` method issues a challenge for the host management UI;
452
+ `consume` verifies the human-approved proof, expiration and full command context
453
+ on the supplied transaction. A caller boolean is never confirmation. Repeated
454
+ successful deletes return the stored receipt without consuming proof again.
455
+
456
+ Apply `migrations/0004_terminal_controls.sql` after `0001_artifacts.sql`,
457
+ `0002_lifecycle.sql` and `0003_coordinator.sql` before activation.
458
+ The default management writer uses the coordinator's PostgreSQL
459
+ transaction; alternative stores implement `management.createUnit` with the same
460
+ parent/ACL/current lock order and transactional effects. Terminal markers and
461
+ SQL guards prevent restoration and later publication. Deletion reports
462
+ `purgeState: "pending"`; physical purge belongs to host retention policy.
463
+
464
+ Get/list include binding, creation/modification dates, current release, digest,
465
+ host URLs and available metadata. Historical modification dates stay nullable.
466
+ The PostgreSQL projection reads the optional metadata column added by the
467
+ publication parity lane without requiring that column for older stores.
@@ -0,0 +1,4 @@
1
+ import type { FinalizeTxClient } from '../server/commit/types.js';
2
+ import type { ManagementUnit } from '../server/management.js';
3
+ /** Parent lock serializes ACL inserts, lifecycle changes and publication FKs. */
4
+ export declare function createPostgresManagementUnit(client: FinalizeTxClient): ManagementUnit;
@@ -0,0 +1,36 @@
1
+ /** Parent lock serializes ACL inserts, lifecycle changes and publication FKs. */
2
+ export function createPostgresManagementUnit(client) {
3
+ return {
4
+ async lock(id) {
5
+ await client.query('SELECT id FROM artifacts.artifacts WHERE id = $1 FOR UPDATE', [id]);
6
+ await client.query('SELECT principal_id FROM artifacts.artifact_access WHERE artifact_id = $1 ORDER BY principal_id FOR UPDATE', [id]);
7
+ await client.query('SELECT release_id FROM artifacts.artifact_current WHERE artifact_id = $1 FOR UPDATE', [id]);
8
+ },
9
+ async read(id) {
10
+ const { rows } = await client.query(`SELECT a.owner_id, a.storage_scope_id, a.lifecycle_state, a.is_public, a.terminal_deleted_at, c.release_id
11
+ FROM artifacts.artifacts a LEFT JOIN artifacts.artifact_current c ON c.artifact_id = a.id WHERE a.id = $1`, [id]);
12
+ const row = rows[0];
13
+ if (!row)
14
+ return undefined;
15
+ const acl = { ownerId: row.owner_id, storageScopeId: row.storage_scope_id,
16
+ isPublic: row.is_public, withdrawn: row.lifecycle_state === 'withdrawn', grants: [] };
17
+ return { acl, state: row.lifecycle_state, terminal: row.terminal_deleted_at !== null,
18
+ currentReleaseId: row.release_id };
19
+ },
20
+ async setAccess(id, principalId, action, updatedAt) {
21
+ const changed = action === 'grant' ? await client.query(`INSERT INTO artifacts.artifact_access (artifact_id, principal_id, permission) VALUES ($1, $2, 'read')
22
+ ON CONFLICT (artifact_id, principal_id) DO NOTHING`, [id, principalId])
23
+ : await client.query('DELETE FROM artifacts.artifact_access WHERE artifact_id = $1 AND principal_id = $2', [id, principalId]);
24
+ await client.query(`UPDATE artifacts.artifacts SET is_public = false, updated_at = $2
25
+ WHERE id = $1 AND ($3::boolean OR is_public)`, [id, updatedAt, changed.rowCount > 0]);
26
+ const result = await client.query('SELECT principal_id, permission FROM artifacts.artifact_access WHERE artifact_id = $1 ORDER BY principal_id', [id]);
27
+ const stamp = await client.query('SELECT updated_at FROM artifacts.artifacts WHERE id = $1', [id]);
28
+ return { grants: result.rows.map((row) => ({ principalId: row.principal_id, grant: row.permission })),
29
+ updatedAt: stamp.rows[0]?.updated_at?.toISOString() ?? null };
30
+ },
31
+ async delete(id, deletedAt) {
32
+ await client.query(`UPDATE artifacts.artifacts SET lifecycle_state = 'deleted', terminal_deleted_at = $2,
33
+ updated_at = $2, is_public = false WHERE id = $1`, [id, deletedAt]);
34
+ },
35
+ };
36
+ }
@@ -1,3 +1,4 @@
1
+ import { ArtifactOperationError } from '../server/failures.js';
1
2
  /** Read-only migration inventory. Never promotes a legacy success into an authorized receipt. */
2
3
  export function createPostgresLegacyArtifactRecords(pool) {
3
4
  return {
@@ -20,6 +21,9 @@ function toIntent(row) {
20
21
  }
21
22
  function toRelease(row) {
22
23
  return {
24
+ authorPrincipalId: row.author_principal_id ?? null,
25
+ parentReleaseId: row.parent_release_id ?? null,
26
+ revertedFromReleaseId: row.reverted_from_release_id ?? null,
23
27
  releaseId: row.id,
24
28
  artifactId: row.artifact_id,
25
29
  manifestDigest: row.manifest_digest,
@@ -34,8 +38,10 @@ export function createPostgresArtifactStore(pool) {
34
38
  async ensureArtifact(input) {
35
39
  const where = `WHERE storage_scope_id = $1 AND owner_id = $2 AND binding_name = $3`;
36
40
  const args = [input.storageScopeId, input.ownerId, input.bindingName];
37
- const found = await pool.query(`SELECT id FROM artifacts.artifacts ${where}`, args);
41
+ const found = await pool.query(`SELECT id, (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a ${where}`, args);
38
42
  if (found.rowCount !== 0 && found.rows[0] !== undefined) {
43
+ if (found.rows[0].terminal)
44
+ throw new ArtifactOperationError('gone');
39
45
  return { artifactId: found.rows[0].id, created: false };
40
46
  }
41
47
  const inserted = await pool.query(`INSERT INTO artifacts.artifacts (id, storage_scope_id, owner_id, binding_name, profile)
@@ -44,10 +50,12 @@ export function createPostgresArtifactStore(pool) {
44
50
  if (inserted.rowCount !== 0 && inserted.rows[0] !== undefined) {
45
51
  return { artifactId: inserted.rows[0].id, created: true };
46
52
  }
47
- const raced = await pool.query(`SELECT id FROM artifacts.artifacts ${where}`, args);
53
+ const raced = await pool.query(`SELECT id, (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a ${where}`, args);
48
54
  const row = raced.rows[0];
49
55
  if (row === undefined)
50
56
  throw new Error(`artifact binding lost for ${input.bindingName}`);
57
+ if (row.terminal)
58
+ throw new ArtifactOperationError('gone');
51
59
  return { artifactId: row.id, created: false };
52
60
  },
53
61
  async record(kind, payload) {
@@ -138,11 +146,14 @@ export function createPostgresArtifactStore(pool) {
138
146
  }
139
147
  },
140
148
  async getRelease(releaseId) {
141
- const { rows } = await pool.query(`SELECT id, artifact_id, manifest_digest, manifest, state, created_at
142
- FROM artifacts.releases WHERE id = $1`, [releaseId]);
149
+ const { rows } = await pool.query(`SELECT * FROM artifacts.releases WHERE id = $1`, [releaseId]);
143
150
  const row = rows[0];
144
151
  return row === undefined ? undefined : toRelease(row);
145
152
  },
153
+ async listReleases(artifactId) {
154
+ const { rows } = await pool.query(`SELECT * FROM artifacts.releases WHERE artifact_id = $1 ORDER BY created_at, id`, [artifactId]);
155
+ return rows.map(toRelease);
156
+ },
146
157
  async getCurrentReleaseId(artifactId) {
147
158
  const { rows } = await pool.query(`SELECT release_id FROM artifacts.artifact_current WHERE artifact_id = $1`, [artifactId]);
148
159
  return rows[0]?.release_id;
@@ -185,14 +196,17 @@ export function createPostgresArtifactStore(pool) {
185
196
  return rows.map((row) => row.digest);
186
197
  },
187
198
  async getArtifactAcl(artifactId) {
188
- const { rows } = await pool.query(`SELECT owner_id, storage_scope_id, is_public, lifecycle_state
189
- FROM artifacts.artifacts WHERE id = $1`, [artifactId]);
199
+ const { rows } = await pool.query(`SELECT owner_id, storage_scope_id, is_public, lifecycle_state, binding_name, created_at,
200
+ to_jsonb(a)->>'updated_at' AS updated_at, to_jsonb(a)->'metadata' AS metadata
201
+ FROM artifacts.artifacts a WHERE id = $1`, [artifactId]);
190
202
  const row = rows[0];
191
203
  if (row === undefined)
192
204
  return undefined;
193
205
  const grants = await pool.query(`SELECT principal_id, permission FROM artifacts.artifact_access
194
206
  WHERE artifact_id = $1 ORDER BY principal_id`, [artifactId]);
195
207
  return {
208
+ bindingName: row.binding_name, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at,
209
+ ...(row.metadata ?? {}),
196
210
  ownerId: row.owner_id,
197
211
  storageScopeId: row.storage_scope_id,
198
212
  isPublic: row.is_public,
@@ -201,14 +215,15 @@ export function createPostgresArtifactStore(pool) {
201
215
  principalId: grant.principal_id,
202
216
  grant: grant.permission,
203
217
  })),
218
+ ...(row.metadata === null || Object.keys(row.metadata).length === 0 ? {} : { metadata: row.metadata }),
204
219
  };
205
220
  },
206
221
  async putArtifactAcl(artifactId, acl) {
207
- await pool.query(`UPDATE artifacts.artifacts SET is_public = $2,
222
+ await pool.query(`UPDATE artifacts.artifacts SET is_public = $2, metadata = $4,
208
223
  lifecycle_state = CASE WHEN $3 THEN 'withdrawn'
209
224
  WHEN lifecycle_state = 'withdrawn' THEN 'active'
210
225
  ELSE lifecycle_state END
211
- WHERE id = $1`, [artifactId, acl.isPublic, acl.withdrawn]);
226
+ WHERE id = $1`, [artifactId, acl.isPublic, acl.withdrawn, JSON.stringify(acl.metadata ?? {})]);
212
227
  await pool.query(`DELETE FROM artifacts.artifact_access WHERE artifact_id = $1`, [
213
228
  artifactId,
214
229
  ]);
@@ -263,6 +278,13 @@ export function createPostgresArtifactStore(pool) {
263
278
  const { rows } = await pool.query(`SELECT lifecycle_state FROM artifacts.artifacts WHERE id = $1`, [artifactId]);
264
279
  return rows[0]?.lifecycle_state ?? 'active';
265
280
  },
281
+ async isTerminal(artifactId) {
282
+ const { rows } = await pool.query("SELECT (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a WHERE id = $1", [artifactId]);
283
+ return rows[0]?.terminal === true;
284
+ },
285
+ async markTerminal(artifactId) {
286
+ await pool.query("UPDATE artifacts.artifacts SET terminal_deleted_at = coalesce(terminal_deleted_at, now()), lifecycle_state = 'deleted' WHERE id = $1", [artifactId]);
287
+ },
266
288
  async setArtifactState(artifactId, state) {
267
289
  const updated = await pool.query(`UPDATE artifacts.artifacts SET lifecycle_state = $2 WHERE id = $1`, [artifactId, state]);
268
290
  if ((updated.rowCount ?? 0) === 0)
package/dist/http.d.ts CHANGED
@@ -7,6 +7,9 @@ import type { AclStore } from './server/access.js';
7
7
  import type { LifecycleService } from './server/lifecycle.js';
8
8
  import { type ArtifactInvocationContext, type ArtifactCommitCoordinator } from './server/commit/types.js';
9
9
  import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
10
+ import { type ManagementUnit, type ArtifactConfirmationPort } from './server/management.js';
11
+ import type { FinalizeTxClient } from './server/commit/types.js';
12
+ import type { SourceStore } from './server/sources.js';
10
13
  export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
11
14
  export interface HttpRequest {
12
15
  method: string;
@@ -37,9 +40,16 @@ export interface HttpAppDeps {
37
40
  ensureHost: GuardedEnsureHost;
38
41
  publication: Omit<GuardedPublicationDeps, 'coordinator' | 'limits'> & {
39
42
  getCurrent: PublicationService['getCurrent'];
43
+ getRelease: PublicationService['getRelease'];
44
+ listReleases: PublicationService['listReleases'];
45
+ };
46
+ sources?: Pick<SourceStore, 'getSource'>;
47
+ management?: {
48
+ createUnit?: (client: FinalizeTxClient) => ManagementUnit;
49
+ confirmation?: ArtifactConfirmationPort;
40
50
  };
41
51
  acls: AclStore;
42
- lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead'>;
52
+ lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead' | 'getReleaseState'>;
43
53
  isApprovedAccount?: (principalId: string) => boolean;
44
54
  /** One trusted URL policy shared by HTTP and MCP receipts. */
45
55
  publicationUrls?: (artifactId: string, releaseId: string) => {