@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.
- package/README.md +74 -4
- package/dist/adapters/postgres-commit.d.ts +15 -0
- package/dist/adapters/postgres-commit.js +80 -0
- package/dist/adapters/postgres.d.ts +5 -2
- package/dist/adapters/postgres.js +39 -0
- package/dist/content/handler.js +79 -63
- package/dist/http.d.ts +42 -29
- package/dist/http.js +168 -491
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-core.d.ts +52 -0
- package/dist/mcp-core.js +111 -0
- package/dist/mcp.d.ts +4 -60
- package/dist/mcp.js +7 -276
- package/dist/profile/contracts.d.ts +67 -0
- package/dist/profile/contracts.js +31 -0
- package/dist/profile/index.d.ts +6 -0
- package/dist/profile/index.js +117 -0
- package/dist/profile/json.d.ts +10 -0
- package/dist/profile/json.js +76 -0
- package/dist/profile/radar.d.ts +3 -0
- package/dist/profile/radar.js +21 -0
- package/dist/profile/schemas.d.ts +6 -0
- package/dist/profile/schemas.js +76 -0
- package/dist/profile/validation.d.ts +5 -0
- package/dist/profile/validation.js +104 -0
- package/dist/profile/validators.d.ts +3 -0
- package/dist/profile/validators.js +51 -0
- package/dist/server/authorized-replay.d.ts +33 -0
- package/dist/server/authorized-replay.js +31 -0
- package/dist/server/command-identity.d.ts +52 -0
- package/dist/server/command-identity.js +71 -0
- package/dist/server/commit/brands.d.ts +7 -0
- package/dist/server/commit/brands.js +3 -0
- package/dist/server/commit/coordinator.d.ts +2 -0
- package/dist/server/commit/coordinator.js +729 -0
- package/dist/server/commit/types.d.ts +379 -0
- package/dist/server/commit/types.js +94 -0
- package/dist/server/commit-contract.d.ts +4 -0
- package/dist/server/commit-contract.js +2 -0
- package/dist/server/failures.d.ts +84 -0
- package/dist/server/failures.js +133 -0
- package/dist/server/gc.d.ts +42 -1
- package/dist/server/gc.js +168 -5
- package/dist/server/idempotency.d.ts +4 -0
- package/dist/server/idempotency.js +14 -0
- package/dist/server/lifecycle.d.ts +2 -1
- package/dist/server/lifecycle.js +15 -1
- package/dist/server/object-intents.d.ts +14 -1
- package/dist/server/object-intents.js +31 -0
- package/dist/server/outcome-contract.d.ts +3 -0
- package/dist/server/outcome-contract.js +3 -0
- package/dist/server/outcome-projection.d.ts +21 -0
- package/dist/server/outcome-projection.js +29 -0
- package/dist/server/preparation-cleanup.d.ts +31 -0
- package/dist/server/preparation-cleanup.js +106 -0
- package/dist/server/publication-limits.d.ts +10 -0
- package/dist/server/publication-limits.js +38 -0
- package/dist/server/publication.d.ts +85 -1
- package/dist/server/publication.js +288 -31
- package/dist/server/service.d.ts +64 -1
- package/dist/server/service.js +187 -1
- package/dist/server/uploads.d.ts +4 -1
- package/dist/server/uploads.js +63 -35
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +86 -0
- package/docs/migration-l6.md +129 -0
- package/examples/radar-commit-setup.ts +299 -0
- package/l0-freeze/command-identity-contract.ts +303 -0
- package/l0-freeze/commit-contract.ts +421 -0
- package/l0-freeze/failure-contract.ts +174 -0
- package/l0-freeze/profile-contract.ts +219 -0
- package/migrations/0003_coordinator.sql +28 -0
- package/package.json +31 -1
- 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
|
|
143
|
-
|
|
144
|
-
|
|
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 {
|
|
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
|
|
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();
|
package/dist/content/handler.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
107
|
-
if (token === undefined)
|
|
93
|
+
catch {
|
|
108
94
|
return fail(401);
|
|
109
|
-
|
|
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
|
-
|
|
133
|
+
response = await handle(request);
|
|
112
134
|
}
|
|
113
135
|
catch {
|
|
114
|
-
|
|
136
|
+
response = fail(500);
|
|
115
137
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
138
|
+
if (request.method === 'HEAD') {
|
|
139
|
+
const { bytes: _bytes, body: _body, ...head } = response;
|
|
140
|
+
return head;
|
|
118
141
|
}
|
|
119
|
-
|
|
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
|
|
2
|
-
import type
|
|
3
|
-
import
|
|
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
|
|
6
|
-
import { type
|
|
7
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
blobs: PublicationBlobStore;
|
|
40
|
+
lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead'>;
|
|
29
41
|
isApprovedAccount?: (principalId: string) => boolean;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
/**
|
|
43
|
-
export declare function
|
|
44
|
-
|
|
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;
|