@sentropic/artifacts 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +74 -4
  2. package/dist/adapters/postgres-commit.d.ts +15 -0
  3. package/dist/adapters/postgres-commit.js +80 -0
  4. package/dist/adapters/postgres.d.ts +5 -2
  5. package/dist/adapters/postgres.js +39 -0
  6. package/dist/content/handler.js +79 -63
  7. package/dist/http.d.ts +42 -29
  8. package/dist/http.js +168 -491
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/mcp-core.d.ts +52 -0
  12. package/dist/mcp-core.js +111 -0
  13. package/dist/mcp.d.ts +4 -60
  14. package/dist/mcp.js +7 -276
  15. package/dist/profile/contracts.d.ts +67 -0
  16. package/dist/profile/contracts.js +31 -0
  17. package/dist/profile/index.d.ts +6 -0
  18. package/dist/profile/index.js +117 -0
  19. package/dist/profile/json.d.ts +10 -0
  20. package/dist/profile/json.js +76 -0
  21. package/dist/profile/radar.d.ts +3 -0
  22. package/dist/profile/radar.js +21 -0
  23. package/dist/profile/schemas.d.ts +6 -0
  24. package/dist/profile/schemas.js +76 -0
  25. package/dist/profile/validation.d.ts +5 -0
  26. package/dist/profile/validation.js +104 -0
  27. package/dist/profile/validators.d.ts +3 -0
  28. package/dist/profile/validators.js +51 -0
  29. package/dist/server/authorized-replay.d.ts +33 -0
  30. package/dist/server/authorized-replay.js +31 -0
  31. package/dist/server/command-identity.d.ts +52 -0
  32. package/dist/server/command-identity.js +71 -0
  33. package/dist/server/commit/brands.d.ts +7 -0
  34. package/dist/server/commit/brands.js +3 -0
  35. package/dist/server/commit/coordinator.d.ts +2 -0
  36. package/dist/server/commit/coordinator.js +729 -0
  37. package/dist/server/commit/types.d.ts +379 -0
  38. package/dist/server/commit/types.js +94 -0
  39. package/dist/server/commit-contract.d.ts +4 -0
  40. package/dist/server/commit-contract.js +2 -0
  41. package/dist/server/failures.d.ts +84 -0
  42. package/dist/server/failures.js +133 -0
  43. package/dist/server/gc.d.ts +42 -1
  44. package/dist/server/gc.js +168 -5
  45. package/dist/server/idempotency.d.ts +4 -0
  46. package/dist/server/idempotency.js +14 -0
  47. package/dist/server/lifecycle.d.ts +2 -1
  48. package/dist/server/lifecycle.js +15 -1
  49. package/dist/server/object-intents.d.ts +14 -1
  50. package/dist/server/object-intents.js +31 -0
  51. package/dist/server/outcome-contract.d.ts +3 -0
  52. package/dist/server/outcome-contract.js +3 -0
  53. package/dist/server/outcome-projection.d.ts +21 -0
  54. package/dist/server/outcome-projection.js +29 -0
  55. package/dist/server/preparation-cleanup.d.ts +31 -0
  56. package/dist/server/preparation-cleanup.js +106 -0
  57. package/dist/server/publication-limits.d.ts +10 -0
  58. package/dist/server/publication-limits.js +38 -0
  59. package/dist/server/publication.d.ts +85 -1
  60. package/dist/server/publication.js +288 -31
  61. package/dist/server/service.d.ts +64 -1
  62. package/dist/server/service.js +187 -1
  63. package/dist/server/uploads.d.ts +4 -1
  64. package/dist/server/uploads.js +63 -35
  65. package/dist/testing.d.ts +50 -0
  66. package/dist/testing.js +86 -0
  67. package/docs/migration-l6.md +129 -0
  68. package/examples/radar-commit-setup.ts +299 -0
  69. package/l0-freeze/command-identity-contract.ts +303 -0
  70. package/l0-freeze/commit-contract.ts +421 -0
  71. package/l0-freeze/failure-contract.ts +174 -0
  72. package/l0-freeze/profile-contract.ts +219 -0
  73. package/migrations/0003_coordinator.sql +28 -0
  74. package/package.json +31 -1
  75. package/src/contracts.ts +125 -0
package/README.md CHANGED
@@ -4,6 +4,76 @@ Generic, host-independent artifact domain state for the Sentropic artifacts
4
4
  provider (`sentropic.artifacts`, spec-v8 §9), covering L-5a–5e. Version
5
5
  `0.1.2` adds UTF-8 text publication, exposes storage adapters and ships SQL migrations.
6
6
  Version `0.1.1` fixed activation ticket release and channel binding (FL-ACT).
7
+ Version `0.1.4` hardens host authority, replay isolation, publication and content
8
+ responses, and adds the platform-free `@sentropic/artifacts/mcp-core` entry.
9
+
10
+ ## 0.2.0 contract line (L0 frozen)
11
+
12
+ The `0.2.0` line freezes the immo #861 declaration shapes only:
13
+ explicit capability selection and the effective publication profile
14
+ (`l0-freeze/profile-contract.ts`), the effective command identity
15
+ (`l0-freeze/command-identity-contract.ts`), the commit finalization unit
16
+ (`l0-freeze/commit-contract.ts`) and the typed failure outcome
17
+ (`l0-freeze/failure-contract.ts`). The section 3.2 host API names are frozen
18
+ against spec r2. Trusted scope/subject domain qualification and
19
+ actor-mapping acceptance remain gated on i-cond Q0; the structural freeze
20
+ is not a completed Q0-dependent qualification. L1+ promotes the shapes
21
+ into `src/`; version reservations are pack-only dev versions owned by the
22
+ integration owner, never registry candidates. The common-contract reuse map lives in
23
+ `docs/common-contract-map.md`; the migration inventory lives in
24
+ `docs/migration-inventory-l0.md`. L0 changes no behavior, no `src/` file
25
+ and no package version.
26
+
27
+ ## Migrating to 0.1.4
28
+
29
+ HTTP hosts must supply a trusted `HttpRequest.storageScopeId` for `/ensure`.
30
+ MCP hosts configure `defaultStorageScopeId` or supply the trusted invocation's
31
+ `ArtifactsInvokeInput.storageScopeId`; tool arguments cannot choose storage.
32
+ Legacy body/tool `storageScopeId` and client `paramsHash` fields remain accepted
33
+ but are ignored. Missing host scope now returns 400. Ensure hashes cover the
34
+ validated parameters, binding, owner and scope; publication hashes cover release
35
+ metadata, server-derived file digests and validated source provenance (including
36
+ its absence and recipe step order). Source writes occur within the idempotent
37
+ publication operation, so unchanged replays do not write provenance again.
38
+ Direct service callers publishing a `source` pass their host `SourceStore` as
39
+ the second argument to `publish`; the HTTP/MCP app supplies its configured store.
40
+ Keys are isolated by operation and
41
+ principal. A changed payload under the same key returns an explicit conflict.
42
+ Persisted unscoped replay keys are no longer consulted after upgrading.
43
+
44
+ Publication defaults to 128 files, 16 MiB per file and 64 MiB total. Hosts can
45
+ override these positive integer limits through `createPublicationService({
46
+ limits: { maxFiles, maxFileBytes, maxTotalBytes } })`; the HTTP decoder uses the
47
+ same byte budgets before allocating bytes. Paths must be unique canonical
48
+ relative paths, at most 1024 UTF-8 bytes: empty/dot segments, backslashes,
49
+ percent escapes, URL delimiters, colons and control characters are rejected.
50
+ Existing integrations using larger bundles or noncanonical paths must adjust
51
+ their limits or filenames. Invalid publications fail before object-store effects.
52
+ Upload sessions cap client `maxSize` by the host `maxUploadBytes` and publication
53
+ file/total limits. Every append checks remaining cumulative capacity before
54
+ decoding bytes or writing staging. Upload publication metadata is validated
55
+ before the session is consumed; a rejected path or MIME type can be corrected
56
+ and committed using the same staged bytes. Finalization validates, hashes and
57
+ publishes one copied snapshot. Overlapping append/commit operations return 409;
58
+ failed verification or metadata validation leaves the session retryable.
59
+
60
+ Content `HEAD` responses have the corresponding GET headers and no body,
61
+ including errors and activation. Errors and activation responses include the
62
+ same security and no-store headers as content responses, with a restrictive CSP.
63
+
64
+ Import `createArtifactsManifest`, `createArtifactsToolExecutor` or
65
+ `createArtifactsAdapter` from `@sentropic/artifacts/mcp-core` when the platform
66
+ is absent. This entry has no runtime platform import. Existing `/mcp` exports,
67
+ including `createArtifactsRegistryDefinition`, retain their platform integration.
68
+ The existing root barrel re-exports `/mcp` and also requires the platform.
69
+ Platform types remain referenced in TypeScript declarations for adapter typing.
70
+ `make probe-artifacts-packed-consumer ENV=test-artifacts-hardening` installs the
71
+ tarball alone in a separate consumer, proves the platform is absent and runs the
72
+ 15-tool manifest and ensure/publish/get/list flow before the platform proof.
73
+
74
+ Release coordination: #707 reserves 0.1.3 for optional platform peer widening;
75
+ 0.1.4 retains `>=0.3.0 <0.5.0` and fits cluster-mesh's current
76
+ `>=0.1.0 <0.2.0` and #707's `>=0.1.3 <0.2.0` artifacts ranges.
7
77
 
8
78
  ## What's here (L-5a)
9
79
 
@@ -137,11 +207,11 @@ a registrable domain. Assets within the same artifact origin remain loadable.
137
207
  method/route/binding checks plus Origin/CSRF evidence for writes.
138
208
  - `src/server/templates.ts` — generic template kit + versioned API guide
139
209
  catalog port (hosts inject approved local assets).
140
- - `src/mcp.ts` — the `sentropic.artifacts` MCP provider: 15 exact
210
+ - `src/mcp-core.ts` — the platform-free `sentropic.artifacts` MCP provider: 15 exact
141
211
  `artifacts_*` tools with closed schemas, a host-bindable invocation
142
- scope, an executor over the same HTTP app, a platform adapter, and the
143
- `@sentropic/mcp-platform` kernel registry definition (the platform is
144
- an optional peer, required only by the `./mcp` subpath).
212
+ scope, an executor over the same HTTP app and a platform adapter.
213
+ - `src/mcp.ts` — the compatible `@sentropic/mcp-platform` kernel registry
214
+ definition wrapper (the optional peer is required here and by the root barrel).
145
215
 
146
216
  Ticket issuance and redemption both require `subjectId`, `artifactId`,
147
217
  `releaseId` and `channel`. The handler checks the release resolved from the
@@ -0,0 +1,15 @@
1
+ /**
2
+ * L3 (immo #861) — PostgreSQL finalization executor (spec r2 §3.1 A4).
3
+ *
4
+ * Owns every coordinator SQL statement against coordinator tables only
5
+ * (`artifacts.operation_keys`, `artifacts.operation_receipts`). No domain
6
+ * table, no policy table and no caller SQL here: host domain reads and
7
+ * writes arrive through the participant and the provider-internal
8
+ * transaction handle. Connections are raw pooled clients; the coordinator
9
+ * owns BEGIN/COMMIT/rollback/release. No nested pool lease spans S3 I/O:
10
+ * preparation happens before the coordinator acquires its client.
11
+ */
12
+ import type { Pool } from 'pg';
13
+ import type { CommitExecutor } from '../server/commit/types.js';
14
+ /** Create the PostgreSQL finalization executor over an injected pool (no DDL). */
15
+ export declare function createPostgresCommitExecutor(pool: Pool): CommitExecutor;
@@ -0,0 +1,80 @@
1
+ function toReceipt(row) {
2
+ return {
3
+ scopeId: row.storage_scope_id,
4
+ internalKey: row.internal_key,
5
+ commandHash: row.command_hash,
6
+ codecVersion: row.command_codec,
7
+ receipt: row.receipt,
8
+ createdAt: row.created_at.toISOString(),
9
+ };
10
+ }
11
+ class PgFinalizeConnection {
12
+ client;
13
+ released = false;
14
+ constructor(client) {
15
+ this.client = client;
16
+ }
17
+ async query(text, params) {
18
+ const result = await this.client.query(text, [...params]);
19
+ return { rows: result.rows, rowCount: result.rowCount ?? 0 };
20
+ }
21
+ async beginReadCommitted() {
22
+ await this.client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
23
+ }
24
+ async lockOperationKey(scopeId, internalKey) {
25
+ await this.client.query(`SELECT 1 FROM artifacts.operation_keys
26
+ WHERE storage_scope_id = $1 AND internal_key = $2 FOR UPDATE`, [scopeId, internalKey]);
27
+ }
28
+ async readReceipt(scopeId, internalKey) {
29
+ const result = await this.client.query(`SELECT storage_scope_id, internal_key, command_codec, command_hash, receipt, created_at
30
+ FROM artifacts.operation_receipts
31
+ WHERE storage_scope_id = $1 AND internal_key = $2`, [scopeId, internalKey]);
32
+ const row = result.rows[0];
33
+ return row === undefined ? undefined : toReceipt(row);
34
+ }
35
+ async insertReceipt(receipt) {
36
+ await this.client.query(`INSERT INTO artifacts.operation_receipts
37
+ (storage_scope_id, internal_key, command_codec, command_hash, receipt)
38
+ VALUES ($1, $2, $3, $4, $5)`, [
39
+ receipt.scopeId,
40
+ receipt.internalKey,
41
+ receipt.codecVersion,
42
+ receipt.commandHash,
43
+ JSON.stringify(receipt.receipt),
44
+ ]);
45
+ }
46
+ async commit() {
47
+ await this.client.query('COMMIT');
48
+ }
49
+ async rollback() {
50
+ await this.client.query('ROLLBACK');
51
+ }
52
+ release(error) {
53
+ if (this.released)
54
+ throw new Error('finalization client already released');
55
+ this.released = true;
56
+ this.client.release(error === undefined ? undefined : error);
57
+ }
58
+ }
59
+ /** Create the PostgreSQL finalization executor over an injected pool (no DDL). */
60
+ export function createPostgresCommitExecutor(pool) {
61
+ return {
62
+ async ensureCoordinationRow(scopeId, internalKey) {
63
+ await pool.query(`INSERT INTO artifacts.operation_keys (storage_scope_id, internal_key)
64
+ VALUES ($1, $2) ON CONFLICT DO NOTHING`, [scopeId, internalKey]);
65
+ },
66
+ async beginFinalization() {
67
+ const client = await pool.connect();
68
+ try {
69
+ await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
70
+ }
71
+ catch (error) {
72
+ // Never hand out a client whose transaction failed to open: destroy
73
+ // it instead of leaking a leased connection the coordinator cannot see.
74
+ client.release(error);
75
+ throw error;
76
+ }
77
+ return new PgFinalizeConnection(client);
78
+ },
79
+ };
80
+ }
@@ -12,9 +12,12 @@ import type { AclStore } from '../server/access.js';
12
12
  import type { RefcountStore } from '../server/gc.js';
13
13
  import type { LifecycleStore } from '../server/lifecycle.js';
14
14
  import type { SourceStore } from '../server/sources.js';
15
- import type { IntentStore } from '../server/object-intents.js';
15
+ import type { PreparationIntentStore } from '../server/object-intents.js';
16
16
  import type { IdempotencyStore } from '../server/idempotency.js';
17
17
  import type { PublicationStateStore } from '../server/publication.js';
18
+ import type { KvPort } from '../server/ports.js';
19
+ /** Read-only migration inventory. Never promotes a legacy success into an authorized receipt. */
20
+ export declare function createPostgresLegacyArtifactRecords(pool: Pool): Pick<KvPort, 'get'>;
18
21
  export interface ArtifactBindingInput {
19
22
  storageScopeId: string;
20
23
  ownerId: string;
@@ -25,7 +28,7 @@ export interface ArtifactBinding {
25
28
  artifactId: string;
26
29
  created: boolean;
27
30
  }
28
- export interface PostgresArtifactBasics extends IntentStore {
31
+ export interface PostgresArtifactBasics extends PreparationIntentStore {
29
32
  ensureArtifact(input: ArtifactBindingInput): Promise<ArtifactBinding>;
30
33
  }
31
34
  export interface PostgresArtifactStore extends PostgresArtifactBasics, PublicationStateStore, IdempotencyStore, AclStore, SourceStore, LifecycleStore, RefcountStore {
@@ -1,3 +1,12 @@
1
+ /** Read-only migration inventory. Never promotes a legacy success into an authorized receipt. */
2
+ export function createPostgresLegacyArtifactRecords(pool) {
3
+ return {
4
+ async get(key) {
5
+ const { rows } = await pool.query('SELECT params_hash, result FROM artifacts.idempotency_keys WHERE key = $1', [key]);
6
+ return rows[0] === undefined ? undefined : JSON.stringify(rows[0]);
7
+ },
8
+ };
9
+ }
1
10
  function toIntent(row) {
2
11
  return {
3
12
  intentId: row.id,
@@ -71,6 +80,36 @@ export function createPostgresArtifactStore(pool) {
71
80
  FROM artifacts.object_intents WHERE state = 'pending' ORDER BY created_at`);
72
81
  return rows.map(toIntent);
73
82
  },
83
+ async listPreparationIntents(query = {}) {
84
+ const { rows } = await pool.query(`SELECT * FROM artifacts.object_intents WHERE kind = 'commit-release' AND state <> 'done'
85
+ AND payload ? 'cleanupDeadlineMs'
86
+ AND ($1::text IS NULL OR payload->>'artifactId' = $1)
87
+ AND ($2::numeric IS NULL OR (COALESCE(error, '') NOT LIKE 'invalid preparation metadata%'
88
+ AND
89
+ CASE WHEN jsonb_typeof(payload->'cleanupDeadlineMs') = 'number'
90
+ THEN (payload->>'cleanupDeadlineMs')::numeric ELSE 0 END <= $2
91
+ AND (payload->>'cleanupState' IS DISTINCT FROM 'failed-cleaned'
92
+ OR (payload->>'cleanupDeadlineMs')::numeric >= $2 - 60000)))
93
+ ORDER BY COALESCE(payload->>'cleanupState' = 'failed-cleaned', false),
94
+ CASE WHEN jsonb_typeof(payload->'cleanupDeadlineMs') = 'number'
95
+ THEN (payload->>'cleanupDeadlineMs')::numeric ELSE 0 END, id
96
+ LIMIT $3`, [query.artifactId ?? null, query.throughMs ?? null, query.limit ?? null]);
97
+ return rows.map(toIntent);
98
+ },
99
+ async claimPreparationCleanup(intentId, throughMs) {
100
+ // This UPDATE serializes with finalization's intent completion on the same row.
101
+ const { rows } = await pool.query(`UPDATE artifacts.object_intents SET state = 'failed',
102
+ error = COALESCE(error, 'preparation expired; explicit retry required'), updated_at = now()
103
+ WHERE id = $1 AND kind = 'commit-release' AND state <> 'done'
104
+ AND (payload->>'cleanupDeadlineMs')::numeric <= $2 RETURNING *`, [intentId, throughMs]);
105
+ return rows[0] === undefined ? undefined : toIntent(rows[0]);
106
+ },
107
+ async settlePreparationCleanup(intentId) {
108
+ const { rowCount } = await pool.query(`UPDATE artifacts.object_intents SET payload = payload || '{"cleanupState":"failed-cleaned"}'::jsonb,
109
+ updated_at = now() WHERE id = $1 AND state = 'failed'`, [intentId]);
110
+ if (rowCount === 0)
111
+ throw new Error('cleanup needs a failed preparation');
112
+ },
74
113
  async putRelease(release) {
75
114
  const parsed = JSON.parse(release.manifest);
76
115
  const client = await pool.connect();
@@ -13,8 +13,15 @@ import { errorBody } from '../http.js';
13
13
  import { CONTENT_GRANT_TTL_SECONDS } from './grants.js';
14
14
  import { contentHeaders, grantCookie, parseGrantCookie } from './headers.js';
15
15
  import { isServiceWorkerPath, parseContentHost, resolveContentPath } from './routing.js';
16
+ function jsonResponse(status, body, extra = {}) {
17
+ return { status, body, headers: {
18
+ ...contentHeaders({ mimeType: 'application/json', size: Buffer.byteLength(JSON.stringify(body)) }),
19
+ 'content-security-policy': "default-src 'none'; frame-ancestors 'none'; base-uri 'none'",
20
+ ...extra,
21
+ } };
22
+ }
16
23
  function fail(status) {
17
- return { status, headers: { 'content-type': 'application/json' }, body: errorBody(status) };
24
+ return jsonResponse(status, errorBody(status));
18
25
  }
19
26
  /** Create the content handler over injected host ports (no server). */
20
27
  export function createContentHandler(deps) {
@@ -52,78 +59,87 @@ export function createContentHandler(deps) {
52
59
  bytes,
53
60
  };
54
61
  }
55
- return {
56
- async handle(request) {
57
- if (request.method !== 'GET' && request.method !== 'HEAD')
58
- return fail(404);
59
- const origin = parseContentHost(request.host, deps.baseDomain);
60
- if (origin === undefined)
61
- return fail(404);
62
- let artifactId;
63
- let release;
64
- if (origin.kind === 'stable') {
65
- artifactId = origin.artifactId;
66
- release = await deps.publication.getCurrent(artifactId);
67
- }
68
- else {
69
- release = await deps.publication.getRelease(origin.releaseId);
70
- if (release === undefined)
71
- return fail(404);
72
- const owner = JSON.parse(release.manifest).artifactId;
73
- if (typeof owner !== 'string')
74
- return fail(404);
75
- artifactId = owner;
76
- }
62
+ async function handle(request) {
63
+ if (request.method !== 'GET' && request.method !== 'HEAD')
64
+ return fail(404);
65
+ const origin = parseContentHost(request.host, deps.baseDomain);
66
+ if (origin === undefined)
67
+ return fail(404);
68
+ let artifactId;
69
+ let release;
70
+ if (origin.kind === 'stable') {
71
+ artifactId = origin.artifactId;
72
+ release = await deps.publication.getCurrent(artifactId);
73
+ }
74
+ else {
75
+ release = await deps.publication.getRelease(origin.releaseId);
77
76
  if (release === undefined)
78
77
  return fail(404);
79
- if (request.ticket !== undefined) {
80
- if (request.actorId === undefined || request.actorId.length === 0)
81
- return fail(401);
82
- try {
83
- await deps.tickets.redeem(request.ticket, {
84
- subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
85
- });
86
- }
87
- catch {
88
- return fail(401);
89
- }
90
- if (!(await canReadNow(artifactId, request.actorId)))
91
- return fail(404);
92
- const token = deps.grants.issue({
93
- subjectId: request.actorId,
94
- artifactId,
95
- releaseId: release.releaseId,
78
+ const owner = JSON.parse(release.manifest).artifactId;
79
+ if (typeof owner !== 'string')
80
+ return fail(404);
81
+ artifactId = owner;
82
+ }
83
+ if (release === undefined)
84
+ return fail(404);
85
+ if (request.ticket !== undefined) {
86
+ if (request.actorId === undefined || request.actorId.length === 0)
87
+ return fail(401);
88
+ try {
89
+ await deps.tickets.redeem(request.ticket, {
90
+ subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
96
91
  });
97
- return {
98
- status: 200,
99
- headers: {
100
- 'content-type': 'application/json',
101
- 'set-cookie': grantCookie(token, CONTENT_GRANT_TTL_SECONDS),
102
- },
103
- body: { activated: true, artifactId, releaseId: release.releaseId },
104
- };
105
92
  }
106
- const token = parseGrantCookie(request.cookie);
107
- if (token === undefined)
93
+ catch {
108
94
  return fail(401);
109
- let claims;
95
+ }
96
+ if (!(await canReadNow(artifactId, request.actorId)))
97
+ return fail(404);
98
+ const token = deps.grants.issue({
99
+ subjectId: request.actorId,
100
+ artifactId,
101
+ releaseId: release.releaseId,
102
+ });
103
+ return jsonResponse(200, { activated: true, artifactId, releaseId: release.releaseId }, {
104
+ 'set-cookie': grantCookie(token, CONTENT_GRANT_TTL_SECONDS),
105
+ });
106
+ }
107
+ const token = parseGrantCookie(request.cookie);
108
+ if (token === undefined)
109
+ return fail(401);
110
+ let claims;
111
+ try {
112
+ claims = deps.grants.verify(token);
113
+ }
114
+ catch {
115
+ return fail(401);
116
+ }
117
+ if (claims.artifactId !== artifactId || claims.releaseId !== release.releaseId) {
118
+ return fail(401); // fresh version consent: activate this release
119
+ }
120
+ if (request.actorId !== undefined && request.actorId !== claims.subjectId)
121
+ return fail(401);
122
+ if (!(await canReadNow(artifactId, claims.subjectId)))
123
+ return fail(404);
124
+ const filePath = resolveContentPath(request.path);
125
+ if (filePath === undefined || isServiceWorkerPath(filePath))
126
+ return fail(404);
127
+ return serveFile(artifactId, release.manifest, filePath);
128
+ }
129
+ return {
130
+ async handle(request) {
131
+ let response;
110
132
  try {
111
- claims = deps.grants.verify(token);
133
+ response = await handle(request);
112
134
  }
113
135
  catch {
114
- return fail(401);
136
+ response = fail(500);
115
137
  }
116
- if (claims.artifactId !== artifactId || claims.releaseId !== release.releaseId) {
117
- return fail(401); // fresh version consent: activate this release
138
+ if (request.method === 'HEAD') {
139
+ const { bytes: _bytes, body: _body, ...head } = response;
140
+ return head;
118
141
  }
119
- if (request.actorId !== undefined && request.actorId !== claims.subjectId)
120
- return fail(401);
121
- if (!(await canReadNow(artifactId, claims.subjectId)))
122
- return fail(404);
123
- const filePath = resolveContentPath(request.path);
124
- if (filePath === undefined || isServiceWorkerPath(filePath))
125
- return fail(404);
126
- return serveFile(artifactId, release.manifest, filePath);
142
+ return response;
127
143
  },
128
144
  };
129
145
  }
package/dist/http.d.ts CHANGED
@@ -1,45 +1,58 @@
1
- import type { ParameterSchema } from './parameters.js';
2
- import type { ArtifactService } from './server/service.js';
3
- import type { PublicationBlobStore, PublicationService } from './server/publication.js';
1
+ import { type CompiledArtifactsProfile } from './profile/index.js';
2
+ import { type ArtifactKeyDerivationBinding } from './server/command-identity.js';
3
+ import { type GuardedEnsureHost } from './server/service.js';
4
+ import type { KvPort } from './server/ports.js';
5
+ import { type GuardedPublicationDeps, type PublicationService } from './server/publication.js';
6
+ import type { AclStore } from './server/access.js';
4
7
  import type { LifecycleService } from './server/lifecycle.js';
5
- import type { ForkService } from './server/fork.js';
6
- import { type AclStore } from './server/access.js';
7
- import { type SourceStore } from './server/sources.js';
8
- import type { StagingStore, UploadService } from './server/uploads.js';
9
- import type { TemplateCatalogPort } from './server/templates.js';
8
+ import { type ArtifactInvocationContext, type ArtifactCommitCoordinator } from './server/commit/types.js';
9
+ import { type ArtifactOutcome } from './server/failures.js';
10
+ export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
10
11
  export interface HttpRequest {
11
12
  method: string;
12
13
  path: string;
13
14
  body: unknown;
14
15
  actorId: string;
16
+ /** Trusted host assertion, never caller authority. */
17
+ storageScopeId?: string;
18
+ /** Optional platform envelope; must equal the tool argument byte for byte. */
19
+ idempotencyKey?: string;
15
20
  }
16
21
  export interface HttpResponse {
17
22
  status: number;
18
23
  body: unknown;
19
24
  }
25
+ /** Stable content-route error codes; private driver messages never leave the host. */
26
+ export declare function errorBody(status: number): {
27
+ error: string;
28
+ };
20
29
  export interface HttpAppDeps {
21
- parameterSchema: ParameterSchema;
22
- artifacts: ArtifactService;
23
- publication: PublicationService;
24
- lifecycle: LifecycleService;
25
- forks: ForkService;
30
+ profile: CompiledArtifactsProfile;
31
+ coordinator: ArtifactCommitCoordinator;
32
+ derivation: ArtifactKeyDerivationBinding;
33
+ /** Explicit pilot-state inventory reader; supply an empty reader only for a verified fresh store. */
34
+ legacyRecords: Pick<KvPort, 'get'>;
35
+ ensureHost: GuardedEnsureHost;
36
+ publication: Omit<GuardedPublicationDeps, 'coordinator' | 'limits'> & {
37
+ getCurrent: PublicationService['getCurrent'];
38
+ };
26
39
  acls: AclStore;
27
- sources: SourceStore;
28
- blobs: PublicationBlobStore;
40
+ lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead'>;
29
41
  isApprovedAccount?: (principalId: string) => boolean;
30
- /** Bounded upload sessions (L-5e); upload routes refuse when unwired. */
31
- uploads?: UploadService;
32
- staging?: StagingStore;
33
- /** Template kit + API guide catalog (L-5e); refuses when unwired. */
34
- templates?: TemplateCatalogPort;
42
+ /** One trusted URL policy shared by HTTP and MCP receipts. */
43
+ publicationUrls?: (artifactId: string, releaseId: string) => {
44
+ stableUrl: string;
45
+ pinnedUrl: string;
46
+ };
35
47
  }
36
- /** Stable error body for one status; internal messages never leave the app. */
37
- export declare function errorBody(status: number): {
38
- error: string;
39
- };
40
- /** Canonical text MIME types; parameters are omitted (encoding is always UTF-8). */
48
+ export interface ArtifactApp {
49
+ readonly selectedCapabilities: CompiledArtifactsProfile['selectedCapabilities'];
50
+ readonly profileDigest: string;
51
+ execute(request: HttpRequest, invocation: ArtifactInvocationContext): Promise<ArtifactOutcome<HttpResponse>>;
52
+ handle(request: HttpRequest, invocation: ArtifactInvocationContext): Promise<HttpResponse>;
53
+ }
54
+ /** Retained for consumers describing canonical textual publication inputs. */
41
55
  export declare const TEXT_MIME_PATTERN = "^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])";
42
- /** Create the HTTP app over injected services (no server, no fetch). */
43
- export declare function createHttpArtifactApp(deps: HttpAppDeps): {
44
- handle(request: HttpRequest): Promise<HttpResponse>;
45
- };
56
+ /** Other mutation surfaces need their own qualification before host activation. */
57
+ export declare function assertArtifactsTransportProfile(value: unknown): asserts value is CompiledArtifactsProfile;
58
+ export declare function createHttpArtifactApp(deps: HttpAppDeps): ArtifactApp;