@sentropic/artifacts 0.0.0-stage → 0.1.1

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -2
  3. package/dist/adapters/postgres.d.ts +35 -0
  4. package/dist/adapters/postgres.js +301 -0
  5. package/dist/adapters/s3.d.ts +19 -0
  6. package/dist/adapters/s3.js +70 -0
  7. package/dist/client.d.ts +23 -0
  8. package/dist/client.js +44 -0
  9. package/dist/codecs.d.ts +12 -0
  10. package/dist/codecs.js +77 -0
  11. package/dist/content/api-profile.d.ts +38 -0
  12. package/dist/content/api-profile.js +40 -0
  13. package/dist/content/grants.d.ts +57 -0
  14. package/dist/content/grants.js +143 -0
  15. package/dist/content/handler.d.ts +34 -0
  16. package/dist/content/handler.js +129 -0
  17. package/dist/content/headers.d.ts +25 -0
  18. package/dist/content/headers.js +56 -0
  19. package/dist/content/routing.d.ts +13 -0
  20. package/dist/content/routing.js +59 -0
  21. package/dist/contracts.d.ts +102 -0
  22. package/dist/contracts.js +25 -0
  23. package/dist/http.d.ts +43 -0
  24. package/dist/http.js +502 -0
  25. package/dist/index.d.ts +25 -0
  26. package/dist/index.js +29 -0
  27. package/dist/mcp.d.ts +68 -0
  28. package/dist/mcp.js +293 -0
  29. package/dist/parameters.d.ts +29 -0
  30. package/dist/parameters.js +65 -0
  31. package/dist/provider-definition.d.ts +36 -0
  32. package/dist/provider-definition.js +45 -0
  33. package/dist/server/access.d.ts +39 -0
  34. package/dist/server/access.js +53 -0
  35. package/dist/server/authorization.d.ts +17 -0
  36. package/dist/server/authorization.js +12 -0
  37. package/dist/server/bindings.d.ts +34 -0
  38. package/dist/server/bindings.js +60 -0
  39. package/dist/server/fork.d.ts +51 -0
  40. package/dist/server/fork.js +75 -0
  41. package/dist/server/gc.d.ts +24 -0
  42. package/dist/server/gc.js +90 -0
  43. package/dist/server/idempotency.d.ts +23 -0
  44. package/dist/server/idempotency.js +38 -0
  45. package/dist/server/lifecycle.d.ts +57 -0
  46. package/dist/server/lifecycle.js +184 -0
  47. package/dist/server/object-intents.d.ts +41 -0
  48. package/dist/server/object-intents.js +62 -0
  49. package/dist/server/ports.d.ts +34 -0
  50. package/dist/server/ports.js +50 -0
  51. package/dist/server/publication.d.ts +67 -0
  52. package/dist/server/publication.js +154 -0
  53. package/dist/server/reader-profile.d.ts +27 -0
  54. package/dist/server/reader-profile.js +18 -0
  55. package/dist/server/runtime.d.ts +33 -0
  56. package/dist/server/runtime.js +44 -0
  57. package/dist/server/service.d.ts +19 -0
  58. package/dist/server/service.js +32 -0
  59. package/dist/server/sources.d.ts +27 -0
  60. package/dist/server/sources.js +56 -0
  61. package/dist/server/templates.d.ts +33 -0
  62. package/dist/server/templates.js +86 -0
  63. package/dist/server/uploads.d.ts +29 -0
  64. package/dist/server/uploads.js +86 -0
  65. package/package.json +162 -4
@@ -0,0 +1,62 @@
1
+ /** In-memory IntentStore double for unit tests. */
2
+ export function createMemoryIntentStore(clock = () => new Date().toISOString(), ids = () => crypto.randomUUID()) {
3
+ const intents = new Map();
4
+ return {
5
+ async record(kind, payload) {
6
+ const now = clock();
7
+ const intent = {
8
+ intentId: ids(),
9
+ kind,
10
+ state: 'pending',
11
+ payload: { ...payload },
12
+ createdAt: now,
13
+ updatedAt: now,
14
+ };
15
+ intents.set(intent.intentId, intent);
16
+ return intent;
17
+ },
18
+ async complete(intentId) {
19
+ const intent = intents.get(intentId);
20
+ if (intent === undefined)
21
+ throw new Error(`unknown intent: ${intentId}`);
22
+ intent.state = 'done';
23
+ intent.updatedAt = clock();
24
+ },
25
+ async markFailed(intentId, error) {
26
+ const intent = intents.get(intentId);
27
+ if (intent === undefined)
28
+ throw new Error(`unknown intent: ${intentId}`);
29
+ intent.state = 'failed';
30
+ intent.error = error;
31
+ intent.updatedAt = clock();
32
+ },
33
+ async get(intentId) {
34
+ return intents.get(intentId);
35
+ },
36
+ async listPending() {
37
+ return [...intents.values()]
38
+ .filter((intent) => intent.state === 'pending')
39
+ .sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
40
+ },
41
+ };
42
+ }
43
+ /**
44
+ * Reconcile pending intents after a crash: the resolver inspects each
45
+ * pending intent (effect present → done, effect missing → failed for
46
+ * explicit retry) and the store records the outcome. Returns the report.
47
+ */
48
+ export async function reconcileIntents(store, resolve) {
49
+ const report = { done: [], failed: [] };
50
+ for (const intent of await store.listPending()) {
51
+ const resolution = await resolve(intent);
52
+ if (resolution === 'done') {
53
+ await store.complete(intent.intentId);
54
+ report.done.push(intent.intentId);
55
+ }
56
+ else {
57
+ await store.markFailed(intent.intentId, 'reconcile: effect missing, explicit retry required');
58
+ report.failed.push(intent.intentId);
59
+ }
60
+ }
61
+ return report;
62
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * L-5a — server ports (spec-v8 §9).
3
+ *
4
+ * Injected host resources for the artifact service: key/value persistence
5
+ * and UUID allocation. The service depends only on these ports — never on a
6
+ * driver, ORM row or network client — so hosts supply Postgres/S3-backed
7
+ * implementations (L-5b) while unit tests inject memory doubles. No eager
8
+ * connections or DDL on import.
9
+ */
10
+ export interface KvPort {
11
+ get(key: string): Promise<string | undefined>;
12
+ /** Set only if absent; returns true when this caller won the creation. */
13
+ putIfAbsent(key: string, value: string): Promise<boolean>;
14
+ }
15
+ export interface UuidPort {
16
+ randomUUID(): string;
17
+ }
18
+ export interface ServerPorts {
19
+ kv: KvPort;
20
+ uuid: UuidPort;
21
+ }
22
+ /** In-memory KvPort double for unit tests (single process). */
23
+ export declare function createMemoryKv(): KvPort;
24
+ /** Crypto-backed UuidPort (Node runtime). */
25
+ export declare function createCryptoUuid(): UuidPort;
26
+ export interface KeyMutex {
27
+ runSerialized<T>(key: string, fn: () => Promise<T>): Promise<T>;
28
+ }
29
+ /**
30
+ * In-process per-key mutex: concurrent callers for one key serialize so
31
+ * exactly one creator wins (single-process scope; multi-process fencing
32
+ * belongs to the L-5b Postgres port).
33
+ */
34
+ export declare function createKeyMutex(): KeyMutex;
@@ -0,0 +1,50 @@
1
+ /** In-memory KvPort double for unit tests (single process). */
2
+ export function createMemoryKv() {
3
+ const store = new Map();
4
+ return {
5
+ get(key) {
6
+ return Promise.resolve(store.get(key));
7
+ },
8
+ putIfAbsent(key, value) {
9
+ if (store.has(key))
10
+ return Promise.resolve(false);
11
+ store.set(key, value);
12
+ return Promise.resolve(true);
13
+ },
14
+ };
15
+ }
16
+ /** Crypto-backed UuidPort (Node runtime). */
17
+ export function createCryptoUuid() {
18
+ return {
19
+ randomUUID() {
20
+ return crypto.randomUUID();
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * In-process per-key mutex: concurrent callers for one key serialize so
26
+ * exactly one creator wins (single-process scope; multi-process fencing
27
+ * belongs to the L-5b Postgres port).
28
+ */
29
+ export function createKeyMutex() {
30
+ const locks = new Map();
31
+ return {
32
+ async runSerialized(key, fn) {
33
+ const previous = locks.get(key) ?? Promise.resolve();
34
+ let release;
35
+ const current = new Promise((res) => {
36
+ release = res;
37
+ });
38
+ locks.set(key, current);
39
+ await previous;
40
+ try {
41
+ return await fn();
42
+ }
43
+ finally {
44
+ release();
45
+ if (locks.get(key) === current)
46
+ locks.delete(key);
47
+ }
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,67 @@
1
+ import type { JsonValue } from '../contracts.js';
2
+ import type { IntentStore } from './object-intents.js';
3
+ import type { IdempotencyStore } from './idempotency.js';
4
+ export interface ReleaseRecord {
5
+ releaseId: string;
6
+ artifactId: string;
7
+ manifestDigest: string;
8
+ manifest: string;
9
+ state: 'ready';
10
+ createdAt: string;
11
+ }
12
+ export interface PublicationStateStore {
13
+ putRelease(release: ReleaseRecord): Promise<void>;
14
+ getRelease(releaseId: string): Promise<ReleaseRecord | undefined>;
15
+ getCurrentReleaseId(artifactId: string): Promise<string | undefined>;
16
+ compareAndSwapCurrent(artifactId: string, expected: string | null, next: string): Promise<boolean>;
17
+ }
18
+ export interface PublicationBlobStore {
19
+ putBlob(key: string, bytes: Uint8Array): Promise<void>;
20
+ getBlob(key: string): Promise<Uint8Array | undefined>;
21
+ /** List keys under a prefix (orphan enumeration for crash recovery/GC). */
22
+ listKeys(prefix: string): Promise<string[]>;
23
+ /** Delete a key (strict: unknown keys fail so callers notice). */
24
+ deleteBlob(key: string): Promise<void>;
25
+ }
26
+ /** Derive the `sha256:<hex>` digest from a digest-addressed blob key. */
27
+ export declare function blobDigestOfKey(key: string): string;
28
+ /** In-memory state-store double for unit tests (synchronous CAS). */
29
+ export declare function createMemoryStateStore(): PublicationStateStore;
30
+ /** In-memory blob-store double for unit tests. */
31
+ export declare function createMemoryBlobStore(): PublicationBlobStore;
32
+ export interface PublicationFileInput {
33
+ path: string;
34
+ mimeType: string;
35
+ bytes: Uint8Array;
36
+ }
37
+ export interface PublishInput {
38
+ ownerId: string;
39
+ storageScopeId: string;
40
+ artifactId: string;
41
+ profile: Record<string, JsonValue | undefined>;
42
+ files: PublicationFileInput[];
43
+ idempotencyKey?: string;
44
+ paramsHash?: string;
45
+ }
46
+ export interface PublishResult {
47
+ releaseId: string;
48
+ manifest: string;
49
+ }
50
+ export interface PublicationService {
51
+ publish(input: PublishInput): Promise<PublishResult>;
52
+ getCurrent(artifactId: string): Promise<{
53
+ releaseId: string;
54
+ manifest: string;
55
+ } | undefined>;
56
+ getRelease(releaseId: string): Promise<{
57
+ releaseId: string;
58
+ manifest: string;
59
+ } | undefined>;
60
+ }
61
+ /** Create a publication service over injected stores (no driver imports). */
62
+ export declare function createPublicationService(deps: {
63
+ state: PublicationStateStore;
64
+ blobs: PublicationBlobStore;
65
+ intents: IntentStore;
66
+ idempotency?: IdempotencyStore;
67
+ }): PublicationService;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * L-5b — publication with intents-first commit (spec-v8 §9).
3
+ *
4
+ * Every commit records a `commit-release` intent before any external object
5
+ * operation, writes blobs under digest-addressed keys, verifies final bytes
6
+ * by read-back (digest first, then size) before the release is marked ready,
7
+ * and CAS-promotes the current pointer. A failed step leaves the intent
8
+ * pending and nothing visible; a lost CAS race is an explicit conflict (the
9
+ * loser's blobs stay addressable orphans for L-5c GC, never a visible
10
+ * release). Repeating a request with the same idempotency key returns the
11
+ * one stored release. State and blob stores are ports; the memory doubles
12
+ * serve unit tests and the Postgres/S3 adapters serve integration.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { canonicalManifest, manifestDigest } from '../codecs.js';
16
+ /** Derive the `sha256:<hex>` digest from a digest-addressed blob key. */
17
+ export function blobDigestOfKey(key) {
18
+ const hex = key.split('/').pop() ?? '';
19
+ if (!/^[0-9a-f]{64}$/.test(hex))
20
+ throw new Error(`invalid blob key: ${key}`);
21
+ return `sha256:${hex}`;
22
+ }
23
+ /** In-memory state-store double for unit tests (synchronous CAS). */
24
+ export function createMemoryStateStore() {
25
+ const releases = new Map();
26
+ const current = new Map();
27
+ return {
28
+ putRelease(release) {
29
+ releases.set(release.releaseId, release);
30
+ return Promise.resolve();
31
+ },
32
+ getRelease(releaseId) {
33
+ return Promise.resolve(releases.get(releaseId));
34
+ },
35
+ getCurrentReleaseId(artifactId) {
36
+ return Promise.resolve(current.get(artifactId));
37
+ },
38
+ compareAndSwapCurrent(artifactId, expected, next) {
39
+ const actual = current.get(artifactId) ?? null;
40
+ if (actual !== expected)
41
+ return Promise.resolve(false);
42
+ current.set(artifactId, next);
43
+ return Promise.resolve(true);
44
+ },
45
+ };
46
+ }
47
+ /** In-memory blob-store double for unit tests. */
48
+ export function createMemoryBlobStore() {
49
+ const blobs = new Map();
50
+ const store = {
51
+ putBlob(key, bytes) {
52
+ blobs.set(key, bytes);
53
+ return Promise.resolve();
54
+ },
55
+ getBlob(key) {
56
+ return Promise.resolve(blobs.get(key));
57
+ },
58
+ listKeys(prefix) {
59
+ return Promise.resolve([...blobs.keys()].filter((key) => key.startsWith(prefix)).sort());
60
+ },
61
+ deleteBlob(key) {
62
+ if (!blobs.has(key))
63
+ throw new Error(`unknown blob: ${key}`);
64
+ blobs.delete(key);
65
+ return Promise.resolve();
66
+ },
67
+ };
68
+ return store;
69
+ }
70
+ function shaHex(bytes) {
71
+ return createHash('sha256').update(bytes).digest('hex');
72
+ }
73
+ /** Create a publication service over injected stores (no driver imports). */
74
+ export function createPublicationService(deps) {
75
+ async function commitOnce(input) {
76
+ const releaseId = crypto.randomUUID();
77
+ const entries = input.files.map((file) => {
78
+ const hex = shaHex(file.bytes);
79
+ return {
80
+ entry: { path: file.path, digest: `sha256:${hex}`, size: file.bytes.length, mimeType: file.mimeType },
81
+ key: `artifacts/${input.storageScopeId}/blobs/sha256/${hex}`,
82
+ bytes: file.bytes,
83
+ };
84
+ });
85
+ const body = {
86
+ schemaVersion: 'artifacts/v1',
87
+ artifactId: input.artifactId,
88
+ storageScopeId: input.storageScopeId,
89
+ ownerId: input.ownerId,
90
+ profile: input.profile,
91
+ files: entries.map((e) => e.entry),
92
+ };
93
+ const digest = manifestDigest(body);
94
+ const manifest = canonicalManifest({ ...body, digest });
95
+ const intent = await deps.intents.record('commit-release', {
96
+ artifactId: input.artifactId,
97
+ releaseId,
98
+ manifestDigest: digest,
99
+ blobKeys: entries.map((e) => e.key),
100
+ });
101
+ for (const { key, bytes } of entries) {
102
+ await deps.blobs.putBlob(key, bytes);
103
+ }
104
+ for (const { key, bytes } of entries) {
105
+ const stored = await deps.blobs.getBlob(key);
106
+ if (stored === undefined || shaHex(stored) !== shaHex(bytes)) {
107
+ throw new Error(`publication digest mismatch for ${key}: read-back failed`);
108
+ }
109
+ if (stored.length !== bytes.length) {
110
+ throw new Error(`publication size mismatch for ${key}: read-back failed`);
111
+ }
112
+ }
113
+ await deps.state.putRelease({
114
+ releaseId,
115
+ artifactId: input.artifactId,
116
+ manifestDigest: digest,
117
+ manifest,
118
+ state: 'ready',
119
+ createdAt: new Date().toISOString(),
120
+ });
121
+ const expected = (await deps.state.getCurrentReleaseId(input.artifactId)) ?? null;
122
+ const won = await deps.state.compareAndSwapCurrent(input.artifactId, expected, releaseId);
123
+ if (!won) {
124
+ await deps.intents.markFailed(intent.intentId, 'commit race lost: explicit conflict');
125
+ throw new Error(`publication conflict for ${input.artifactId}: concurrent commit won`);
126
+ }
127
+ await deps.intents.complete(intent.intentId);
128
+ return { releaseId, manifest };
129
+ }
130
+ return {
131
+ async publish(input) {
132
+ if (input.idempotencyKey !== undefined && deps.idempotency !== undefined) {
133
+ const { result } = await deps.idempotency.execute(input.idempotencyKey, input.paramsHash ?? '', () => commitOnce(input));
134
+ return result;
135
+ }
136
+ return commitOnce(input);
137
+ },
138
+ async getCurrent(artifactId) {
139
+ const releaseId = await deps.state.getCurrentReleaseId(artifactId);
140
+ if (releaseId === undefined)
141
+ return undefined;
142
+ const release = await deps.state.getRelease(releaseId);
143
+ if (release === undefined)
144
+ return undefined;
145
+ return { releaseId: release.releaseId, manifest: release.manifest };
146
+ },
147
+ async getRelease(releaseId) {
148
+ const release = await deps.state.getRelease(releaseId);
149
+ if (release === undefined)
150
+ return undefined;
151
+ return { releaseId: release.releaseId, manifest: release.manifest };
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * L-5d — reader-profile contract (spec-v8 §9 5d).
3
+ *
4
+ * Pure evaluation of who may read what, where: exact method/route matches
5
+ * (never prefixes), optional artifact/release bindings, owner bypass, then
6
+ * listed subjects, then approved accounts only when the profile allows
7
+ * them. Deny by default; approval comes from the host via an injected
8
+ * predicate (memory double in tests).
9
+ */
10
+ export interface ReaderRequest {
11
+ subject: string;
12
+ method: string;
13
+ route: string;
14
+ artifactId?: string;
15
+ releaseId?: string;
16
+ }
17
+ export interface ReaderProfile {
18
+ ownerId?: string;
19
+ subjects: string[];
20
+ allowApprovedAccounts: boolean;
21
+ methods: string[];
22
+ routes: string[];
23
+ artifactId?: string;
24
+ releaseId?: string;
25
+ }
26
+ /** Evaluate a reader request against a profile (deny by default). */
27
+ export declare function evaluateReaderProfile(profile: ReaderProfile, request: ReaderRequest, isApprovedAccount?: (principalId: string) => boolean): boolean;
@@ -0,0 +1,18 @@
1
+ /** Evaluate a reader request against a profile (deny by default). */
2
+ export function evaluateReaderProfile(profile, request, isApprovedAccount = () => false) {
3
+ if (request.subject.length === 0)
4
+ return false;
5
+ if (!profile.methods.includes(request.method))
6
+ return false;
7
+ if (!profile.routes.includes(request.route))
8
+ return false;
9
+ if (profile.artifactId !== undefined && request.artifactId !== profile.artifactId)
10
+ return false;
11
+ if (profile.releaseId !== undefined && request.releaseId !== profile.releaseId)
12
+ return false;
13
+ if (profile.ownerId !== undefined && request.subject === profile.ownerId)
14
+ return true;
15
+ if (profile.subjects.includes(request.subject))
16
+ return true;
17
+ return profile.allowApprovedAccounts && isApprovedAccount(request.subject);
18
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * L-5d — generic content runtime (spec-v8 §9 5d).
3
+ *
4
+ * Serve current release bytes to authorized readers over injected host
5
+ * ports (no driver imports). Terminal states report instead of serving;
6
+ * digests outside the current manifest never oracle (404); stored bytes
7
+ * are checked against the manifest size before serving.
8
+ */
9
+ import type { AclStore } from './access.js';
10
+ import type { LifecycleService } from './lifecycle.js';
11
+ import type { PublicationBlobStore, PublicationService } from './publication.js';
12
+ export interface ContentDeps {
13
+ acls: AclStore;
14
+ lifecycle: LifecycleService;
15
+ publication: PublicationService;
16
+ blobs: PublicationBlobStore;
17
+ }
18
+ export interface ServeFileInput {
19
+ artifactId: string;
20
+ digest: string;
21
+ actorId: string;
22
+ isApproved?: (principalId: string) => boolean;
23
+ }
24
+ export interface ServedFile {
25
+ digest: string;
26
+ mimeType: string;
27
+ size: number;
28
+ bytes: Uint8Array;
29
+ }
30
+ /** Create the content runtime over injected host ports. */
31
+ export declare function createContentRuntime(deps: ContentDeps): {
32
+ serveFile(input: ServeFileInput): Promise<ServedFile>;
33
+ };
@@ -0,0 +1,44 @@
1
+ /** Create the content runtime over injected host ports. */
2
+ export function createContentRuntime(deps) {
3
+ return {
4
+ async serveFile(input) {
5
+ const isApproved = input.isApproved ?? (() => false);
6
+ const acl = await deps.acls.getArtifactAcl(input.artifactId);
7
+ if (acl === undefined)
8
+ throw new Error(`unknown artifact: ${input.artifactId}`);
9
+ if (acl.storageScopeId === undefined) {
10
+ throw new Error('invalid request: missing storage scope');
11
+ }
12
+ const state = await deps.lifecycle.stateOf(input.artifactId);
13
+ if (state === 'deleted' || state === 'purged') {
14
+ throw new Error(`cannot serve ${state} artifact: terminal ${input.artifactId}`);
15
+ }
16
+ const readable = deps.lifecycle.canRead({ artifactId: input.artifactId, state }, acl, input.actorId, 'read', isApproved);
17
+ if (!readable) {
18
+ throw new Error(`access denied: ${input.actorId} cannot read ${input.artifactId}`);
19
+ }
20
+ const current = await deps.publication.getCurrent(input.artifactId);
21
+ if (current === undefined)
22
+ throw new Error(`no release to serve for ${input.artifactId}`);
23
+ const manifest = JSON.parse(current.manifest);
24
+ const file = (manifest.files ?? []).find((entry) => entry.digest === input.digest);
25
+ if (file === undefined || typeof file.digest !== 'string') {
26
+ throw new Error(`unknown blob: ${input.digest} for ${input.artifactId}`);
27
+ }
28
+ const hex = file.digest.startsWith('sha256:') ? file.digest.slice('sha256:'.length) : file.digest;
29
+ const key = `artifacts/${acl.storageScopeId}/blobs/sha256/${hex}`;
30
+ const bytes = await deps.blobs.getBlob(key);
31
+ if (bytes === undefined)
32
+ throw new Error(`unknown blob: ${file.digest} for ${input.artifactId}`);
33
+ if (typeof file.size === 'number' && bytes.length !== file.size) {
34
+ throw new Error(`size mismatch for ${file.digest}: manifest ${file.size}, stored ${bytes.length}`);
35
+ }
36
+ return {
37
+ digest: file.digest,
38
+ mimeType: typeof file.mimeType === 'string' ? file.mimeType : 'application/octet-stream',
39
+ size: bytes.length,
40
+ bytes,
41
+ };
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,19 @@
1
+ import type { ServerPorts } from './ports.js';
2
+ export interface EnsurePrivateViewInput {
3
+ actorUserId: string;
4
+ ownerId: string;
5
+ storageScopeId: string;
6
+ bindingName: string;
7
+ idempotencyKey: string;
8
+ paramsHash: string;
9
+ }
10
+ export interface EnsurePrivateViewResult {
11
+ artifactId: string;
12
+ created: boolean;
13
+ replayed: boolean;
14
+ }
15
+ export interface ArtifactService {
16
+ ensurePrivateView(input: EnsurePrivateViewInput): Promise<EnsurePrivateViewResult>;
17
+ }
18
+ /** Create the artifact service over explicit injected ports (no defaults). */
19
+ export declare function createArtifactService(ports: ServerPorts): ArtifactService;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * L-5a — artifact service (spec-v8 §9).
3
+ *
4
+ * One service for both HTTP and MCP: it composes authorization, idempotent
5
+ * execution and stable bindings in a fixed order. Authorization runs first
6
+ * so a forged actor creates nothing — no binding, no idempotency record.
7
+ * Storage and UUIDs come from injected ServerPorts only; there is no
8
+ * implicit default store (fail-closed construction).
9
+ */
10
+ import { assertArtifactAccess } from './authorization.js';
11
+ import { createBindingService } from './bindings.js';
12
+ import { createIdempotencyStore } from './idempotency.js';
13
+ /** Create the artifact service over explicit injected ports (no defaults). */
14
+ export function createArtifactService(ports) {
15
+ const bindings = createBindingService(ports.kv, ports.uuid);
16
+ const idempotent = createIdempotencyStore(ports.kv);
17
+ return {
18
+ async ensurePrivateView(input) {
19
+ assertArtifactAccess({
20
+ actorUserId: input.actorUserId,
21
+ artifactOwnerId: input.ownerId,
22
+ action: 'mutate',
23
+ });
24
+ const { result, replayed } = await idempotent.execute(input.idempotencyKey, input.paramsHash, () => bindings.ensure({
25
+ ownerId: input.ownerId,
26
+ storageScopeId: input.storageScopeId,
27
+ bindingName: input.bindingName,
28
+ }));
29
+ return { artifactId: result.artifactId, created: result.created, replayed };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * L-5c — source references (spec-v8 §9 5c).
3
+ *
4
+ * A source is explicit provenance attached to an artifact: the original
5
+ * inline bytes, a base commit, or a build recipe. Validation is strict at
6
+ * attach time (known kind, non-empty commit/steps) — sources are declared,
7
+ * never inferred from content.
8
+ */
9
+ export type SourceRef = {
10
+ kind: 'inline-original';
11
+ } | {
12
+ kind: 'base-commit';
13
+ commit: string;
14
+ } | {
15
+ kind: 'recipe';
16
+ steps: string[];
17
+ };
18
+ /** Parse an untrusted source payload into a validated reference (no casts). */
19
+ export declare function parseSourceRef(value: unknown): SourceRef;
20
+ /** Validate and attach a source reference (fail-closed on unknown kinds). */
21
+ export declare function attachSource(source: SourceRef): SourceRef;
22
+ export interface SourceStore {
23
+ getSource(artifactId: string): Promise<SourceRef | undefined>;
24
+ putSource(artifactId: string, source: SourceRef): Promise<void>;
25
+ }
26
+ /** In-memory SourceStore double for unit tests. */
27
+ export declare function createMemorySourceStore(): SourceStore;
@@ -0,0 +1,56 @@
1
+ /** Parse an untrusted source payload into a validated reference (no casts). */
2
+ export function parseSourceRef(value) {
3
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
4
+ throw new Error('invalid source: must be an object');
5
+ }
6
+ const record = value;
7
+ switch (record['kind']) {
8
+ case 'inline-original':
9
+ return attachSource({ kind: 'inline-original' });
10
+ case 'base-commit': {
11
+ const commit = record['commit'];
12
+ if (typeof commit !== 'string')
13
+ throw new Error('invalid source: missing commit');
14
+ return attachSource({ kind: 'base-commit', commit });
15
+ }
16
+ case 'recipe': {
17
+ const steps = record['steps'];
18
+ if (!Array.isArray(steps) || steps.some((step) => typeof step !== 'string')) {
19
+ throw new Error('invalid source: steps must be strings');
20
+ }
21
+ return attachSource({ kind: 'recipe', steps: steps });
22
+ }
23
+ default:
24
+ throw new Error(`invalid source: unknown kind ${String(record['kind'])}`);
25
+ }
26
+ }
27
+ /** Validate and attach a source reference (fail-closed on unknown kinds). */
28
+ export function attachSource(source) {
29
+ switch (source.kind) {
30
+ case 'inline-original':
31
+ return { kind: 'inline-original' };
32
+ case 'base-commit':
33
+ if (source.commit.length === 0)
34
+ throw new Error('invalid source: empty commit');
35
+ return { kind: 'base-commit', commit: source.commit };
36
+ case 'recipe':
37
+ if (source.steps.length === 0)
38
+ throw new Error('invalid source: empty steps');
39
+ return { kind: 'recipe', steps: [...source.steps] };
40
+ default:
41
+ throw new Error(`invalid source: unknown kind ${source.kind}`);
42
+ }
43
+ }
44
+ /** In-memory SourceStore double for unit tests. */
45
+ export function createMemorySourceStore() {
46
+ const sources = new Map();
47
+ return {
48
+ getSource(artifactId) {
49
+ return Promise.resolve(sources.get(artifactId));
50
+ },
51
+ putSource(artifactId, source) {
52
+ sources.set(artifactId, source);
53
+ return Promise.resolve();
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * L-5e — template kit + API guide catalog (spec-v8 §9 5e).
3
+ *
4
+ * The generic provider ships a minimal static-web kit and a versioned guide
5
+ * describing the artifacts_* surface. Hosts inject their own catalog port
6
+ * (approved template/guide assets); the default catalog carries no
7
+ * credentials, no host data and no external URLs. A template kit contains
8
+ * approved sources plus a build recipe — never executable server builds.
9
+ */
10
+ export interface TemplateFile {
11
+ path: string;
12
+ content: string;
13
+ }
14
+ export interface ArtifactTemplate {
15
+ templateId: string;
16
+ version: string;
17
+ description: string;
18
+ files: TemplateFile[];
19
+ buildRecipe: string[];
20
+ }
21
+ export interface ApiGuide {
22
+ version: string;
23
+ markdown: string;
24
+ }
25
+ export interface TemplateCatalogPort {
26
+ getTemplate(templateId: string): Promise<ArtifactTemplate | undefined>;
27
+ /** Omit the version for the catalog's current guide. */
28
+ getApiGuide(version?: string): Promise<ApiGuide | undefined>;
29
+ }
30
+ export declare const DEFAULT_TEMPLATE_ID = "static-html";
31
+ export declare const DEFAULT_API_GUIDE_VERSION = "v1";
32
+ /** Generic in-package catalog; hosts override with approved local assets. */
33
+ export declare function createDefaultTemplateCatalog(): TemplateCatalogPort;