@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
@@ -0,0 +1,219 @@
1
+ /**
2
+ * L0 frozen profile declaration (immo #861, ask 1 shape, spec r2 section 2).
3
+ *
4
+ * Stable data shapes, bounds and version constants for explicit capability
5
+ * selection and the effective publication profile, plus the frozen
6
+ * provider-profile interfaces L1 compiles. No behavior: L1 wires these
7
+ * shapes into the factories.
8
+ */
9
+
10
+ import type { EffectiveArtifactCommand } from './command-identity-contract.js';
11
+ import type { JsonValue } from '../src/contracts.js';
12
+
13
+ /** Version of the frozen effective-profile shape. */
14
+ export const PROFILE_CONTRACT_VERSION = 'artifacts-profile/v1' as const;
15
+
16
+ /** All fifteen existing tool names, frozen (L1 wires the compiler to this set). */
17
+ export const KNOWN_ARTIFACT_TOOL_NAMES = [
18
+ 'artifacts_get_template', 'artifacts_get_api_guide', 'artifacts_ensure',
19
+ 'artifacts_publish', 'artifacts_create_upload', 'artifacts_commit_upload',
20
+ 'artifacts_list', 'artifacts_get', 'artifacts_get_source', 'artifacts_fork',
21
+ 'artifacts_set_access', 'artifacts_promote', 'artifacts_renew',
22
+ 'artifacts_delete', 'artifacts_restore',
23
+ ] as const;
24
+
25
+ export type KnownArtifactToolName = (typeof KNOWN_ARTIFACT_TOOL_NAMES)[number];
26
+
27
+ /** Radar ask-1 selection: the only four tools the Radar host enables. */
28
+ export const RADAR_SELECTED_TOOL_NAMES = [
29
+ 'artifacts_ensure', 'artifacts_publish', 'artifacts_list', 'artifacts_get',
30
+ ] as const;
31
+
32
+ export type RadarSelectedToolName = (typeof RADAR_SELECTED_TOOL_NAMES)[number];
33
+
34
+ /**
35
+ * Frozen C2 capability routes: selected capability, provider route and
36
+ * Radar command. The adapter accepts exactly these four method/path
37
+ * combinations, not an arbitrary HTTP proxy.
38
+ */
39
+ export const RADAR_OPERATION_ROUTES = [
40
+ { capability: 'artifacts_ensure', method: 'POST', path: '/ensure', command: 'ensure' },
41
+ { capability: 'artifacts_publish', method: 'POST', path: '/publish', command: 'publish' },
42
+ { capability: 'artifacts_list', method: 'POST', path: '/list', command: 'list' },
43
+ { capability: 'artifacts_get', method: 'GET', path: '/meta/{artifactId}', command: 'meta' },
44
+ ] as const;
45
+
46
+ export type RadarOperationRoute = (typeof RADAR_OPERATION_ROUTES)[number];
47
+
48
+ /** Host-bound invocation scope for the Radar selection. */
49
+ export const RADAR_INVOCATION_SCOPE = 'immo:read' as const;
50
+
51
+ /** Explicit capability selection: unknown and duplicate entries are rejected. */
52
+ export interface ToolSelection {
53
+ readonly tools: readonly string[];
54
+ }
55
+
56
+ /** Closed effective-profile MIME allowlist for publish files. */
57
+ export const PROFILE_TEXT_MIME_TYPES = [
58
+ 'text/html', 'text/css', 'text/javascript', 'application/json',
59
+ ] as const;
60
+
61
+ export type ProfileMimeType = (typeof PROFILE_TEXT_MIME_TYPES)[number];
62
+
63
+ /** First path segments a publish file must never use. */
64
+ export const PROFILE_RESERVED_FIRST_SEGMENTS = [
65
+ 'api', '__artifacts', 'livez', 'health',
66
+ ] as const;
67
+
68
+ /** Frozen C4 bounds (UTF-16 code units unless noted). */
69
+ export const PROFILE_BOUNDS = {
70
+ bindingNameMinUnits: 1,
71
+ bindingNameMaxUnits: 128,
72
+ idempotencyKeyMaxUnits: 256,
73
+ publishFilesMin: 1,
74
+ publishFilesMax: 32,
75
+ maxDecodedFileBytes: 262144,
76
+ maxDecodedTotalBytes: 262144,
77
+ /** Candidate ceiling inherited from the existing provider validator. */
78
+ pathMaxBytes: 1024,
79
+ listLimitMin: 1,
80
+ listLimitMax: 100,
81
+ listLimitDefault: 20,
82
+ listOffsetDefault: 0,
83
+ /** The HTTP control route keeps its separate encoded-body budget. */
84
+ httpControlMaxEncodedBytes: 2 * 1024 * 1024,
85
+ } as const;
86
+
87
+ /** Inputs that participate in composition integrity evidence. */
88
+ export const COMPOSITION_DIGEST_INPUTS = [
89
+ 'selection', 'profile', 'providerContractRevisions',
90
+ ] as const;
91
+
92
+ /** One operation profile inside the effective profile. */
93
+ export interface OperationProfile {
94
+ readonly operation: string;
95
+ readonly limits: Record<string, number>;
96
+ readonly defaults: Record<string, JsonValue | undefined>;
97
+ }
98
+
99
+ /** Immutable versioned effective profile driving one composition. */
100
+ export interface EffectiveProfile {
101
+ readonly version: typeof PROFILE_CONTRACT_VERSION;
102
+ readonly selection: ToolSelection;
103
+ readonly operations: readonly OperationProfile[];
104
+ readonly contractRevisions: Record<string, string>;
105
+ readonly profileDigest: string;
106
+ }
107
+
108
+ /**
109
+ * Frozen provider profile input: explicit selected tools, complete
110
+ * operation profiles and required contract versions. Unknown and
111
+ * duplicate entries are rejected; an empty selection exposes and
112
+ * executes nothing; a missing selection fails construction.
113
+ */
114
+ export interface ArtifactProviderProfile {
115
+ readonly version: typeof PROFILE_CONTRACT_VERSION;
116
+ readonly selectedCapabilities: readonly string[];
117
+ readonly operations: readonly OperationProfile[];
118
+ readonly contractRevisions: Record<string, string>;
119
+ }
120
+
121
+ /**
122
+ * Compiled profile driving schema generation, validation, normalization,
123
+ * defaults, command hashing and runtime limits. Selection, full profile
124
+ * data and provider-contract revisions participate in the digest; a
125
+ * semantic-only change invalidates the evidence even when JSON Schema
126
+ * text is unchanged.
127
+ */
128
+ export interface CompiledArtifactsProfile extends ArtifactProviderProfile {
129
+ readonly schemas: Record<string, Record<string, unknown>>;
130
+ readonly validators: CompiledValidators;
131
+ readonly profileDigest: string;
132
+ }
133
+
134
+ /**
135
+ * Compiled semantic validators and normalizers bound at profile compile
136
+ * time. L1 implements them against the frozen operation profiles; L2/L6
137
+ * consume them instead of reimplementing checks.
138
+ */
139
+ export interface CompiledValidators {
140
+ readonly validateBindingName: (value: unknown) => boolean;
141
+ readonly validateCallerKey: (value: unknown) => boolean;
142
+ readonly validateFileEntry: (entry: unknown) => boolean;
143
+ readonly normalizeCommand: (command: unknown) => EffectiveArtifactCommand;
144
+ }
145
+
146
+ /** Compiler signature: L1 implements it against these frozen shapes. */
147
+ export type CompileArtifactsProfile = (
148
+ input: ArtifactProviderProfile,
149
+ ) => CompiledArtifactsProfile;
150
+
151
+ /**
152
+ * Semantic-only profile pair: identical expressible JSON Schema text with
153
+ * different effective semantics. The digest MUST invalidate on such a
154
+ * change even when schema text is unchanged.
155
+ */
156
+ export const PROFILE_SEMANTIC_PAIR = {
157
+ schemaText: '{"type":"object","properties":{"files":{"type":"array"}}}',
158
+ first: { operation: 'publish', limits: { maxFiles: 32, maxTotalBytes: 262144 }, defaults: {} },
159
+ second: { operation: 'publish', limits: { maxFiles: 16, maxTotalBytes: 262144 }, defaults: {} },
160
+ } as const;
161
+
162
+ const isKnownToolName = (value: string): boolean =>
163
+ (KNOWN_ARTIFACT_TOOL_NAMES as readonly string[]).includes(value);
164
+
165
+ /** Validate an explicit selection: reject unknown and duplicate entries. */
166
+ export function validateToolSelection(selection: ToolSelection): string[] {
167
+ const errors: string[] = [];
168
+ if (!Array.isArray(selection.tools)) return ['selection tools must be an array'];
169
+ const seen = new Set<string>();
170
+ for (const tool of selection.tools) {
171
+ if (typeof tool !== 'string' || !isKnownToolName(tool)) {
172
+ errors.push(`unknown capability: ${String(tool)}`);
173
+ continue;
174
+ }
175
+ if (seen.has(tool)) errors.push(`duplicate capability: ${tool}`);
176
+ seen.add(tool);
177
+ }
178
+ return errors;
179
+ }
180
+
181
+ /** Binding names are required strings of 1-128 UTF-16 code units, unnormalized. */
182
+ export function validateBindingName(value: unknown): boolean {
183
+ return typeof value === 'string' && value.length >= 1 && value.length <= 128;
184
+ }
185
+
186
+ /**
187
+ * Caller idempotency keys: original string, trim() must be nonempty,
188
+ * at most 256 UTF-16 code units. Whitespace around a nonempty value is
189
+ * retained; no trimming or Unicode normalization is applied. Lone high
190
+ * or low surrogates are rejected before UTF-8 conversion, derivation or
191
+ * storage so distinct caller keys can never alias one durable identity
192
+ * through replacement-character encoding.
193
+ */
194
+ export function validateCallerIdempotencyKey(value: unknown): boolean {
195
+ if (typeof value !== 'string') return false;
196
+ if (value.length > 256 || value.trim().length === 0) return false;
197
+ for (let i = 0; i < value.length; i++) {
198
+ const unit = value.charCodeAt(i);
199
+ if (unit < 0xd800 || unit > 0xdfff) continue;
200
+ if (unit > 0xdbff) return false;
201
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
202
+ if (next < 0xdc00 || next > 0xdfff) return false;
203
+ i++;
204
+ }
205
+ return true;
206
+ }
207
+
208
+ /** Radar publication files carry 1-32 entries within the decoded-byte budget. */
209
+ export function validatePublishFileCount(count: unknown): boolean {
210
+ return typeof count === 'number' && Number.isInteger(count) && count >= 1 && count <= 32;
211
+ }
212
+
213
+ /** List bounds: integer limit 1-100, nonnegative integer offset. */
214
+ export function validateListBounds(limit: unknown, offset: unknown): boolean {
215
+ return (
216
+ typeof limit === 'number' && Number.isInteger(limit) && limit >= 1 && limit <= 100 &&
217
+ typeof offset === 'number' && Number.isInteger(offset) && offset >= 0
218
+ );
219
+ }
@@ -0,0 +1,28 @@
1
+ -- L3 (immo #861) — atomic finalization coordination (spec r2 §3.1 A4).
2
+ --
3
+ -- Additive only: no ALTER/DROP of existing objects. Two coordinator-owned
4
+ -- tables. artifacts.operation_keys is the per-key serialization row keyed by
5
+ -- (storage_scope_id, internal_key); it carries no success or authorization
6
+ -- assertion. artifacts.operation_receipts holds exactly one success row per
7
+ -- (storage_scope_id, internal_key) with the command codec, command hash and
8
+ -- receipt; the primary key is the uniqueness fence. This migration is
9
+ -- distinct from artifacts.idempotency_keys, whose success-row count MUST
10
+ -- stay unchanged after a denial. The host applies this file through its
11
+ -- existing journal after 0002_lifecycle.sql.
12
+
13
+ CREATE TABLE IF NOT EXISTS artifacts.operation_keys (
14
+ storage_scope_id TEXT NOT NULL,
15
+ internal_key TEXT NOT NULL,
16
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
17
+ PRIMARY KEY (storage_scope_id, internal_key)
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS artifacts.operation_receipts (
21
+ storage_scope_id TEXT NOT NULL,
22
+ internal_key TEXT NOT NULL,
23
+ command_codec TEXT NOT NULL,
24
+ command_hash TEXT NOT NULL,
25
+ receipt JSONB NOT NULL,
26
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
27
+ PRIMARY KEY (storage_scope_id, internal_key)
28
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Generic configurable artifacts MCP and artifact service: StorageScope/Artifact/Binding/Release model, canonical manifests, object authorization, idempotent intents (spec-v8 §9).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,11 @@
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
+ "imports": {
15
+ "#artifacts-frozen-commit": {
16
+ "types": "./l0-freeze/commit-contract.ts"
17
+ }
18
+ },
14
19
  "exports": {
15
20
  ".": {
16
21
  "types": "./dist/index.d.ts",
@@ -32,6 +37,10 @@
32
37
  "types": "./dist/adapters/postgres.d.ts",
33
38
  "import": "./dist/adapters/postgres.js"
34
39
  },
40
+ "./adapters/postgres-commit": {
41
+ "types": "./dist/adapters/postgres-commit.d.ts",
42
+ "import": "./dist/adapters/postgres-commit.js"
43
+ },
35
44
  "./migrations/*": "./migrations/*",
36
45
  "./server/ports": {
37
46
  "types": "./dist/server/ports.d.ts",
@@ -45,6 +54,18 @@
45
54
  "types": "./dist/server/authorization.d.ts",
46
55
  "import": "./dist/server/authorization.js"
47
56
  },
57
+ "./server/commit-contract": {
58
+ "types": "./dist/server/commit-contract.d.ts",
59
+ "import": "./dist/server/commit-contract.js"
60
+ },
61
+ "./server/outcome-contract": {
62
+ "types": "./dist/server/outcome-contract.d.ts",
63
+ "import": "./dist/server/outcome-contract.js"
64
+ },
65
+ "./testing": {
66
+ "types": "./dist/testing.d.ts",
67
+ "import": "./dist/testing.js"
68
+ },
48
69
  "./server/bindings": {
49
70
  "types": "./dist/server/bindings.d.ts",
50
71
  "import": "./dist/server/bindings.js"
@@ -101,6 +122,10 @@
101
122
  "types": "./dist/mcp.d.ts",
102
123
  "import": "./dist/mcp.js"
103
124
  },
125
+ "./mcp-core": {
126
+ "types": "./dist/mcp-core.d.ts",
127
+ "import": "./dist/mcp-core.js"
128
+ },
104
129
  "./content/grants": {
105
130
  "types": "./dist/content/grants.d.ts",
106
131
  "import": "./dist/content/grants.js"
@@ -140,7 +165,11 @@
140
165
  },
141
166
  "files": [
142
167
  "dist",
168
+ "l0-freeze",
169
+ "src/contracts.ts",
143
170
  "migrations",
171
+ "docs/migration-l6.md",
172
+ "examples/radar-commit-setup.ts",
144
173
  "README.md",
145
174
  "LICENSE"
146
175
  ],
@@ -151,6 +180,7 @@
151
180
  "scripts": {
152
181
  "build": "tsc -p tsconfig.json",
153
182
  "typecheck": "tsc --noEmit -p tsconfig.json",
183
+ "freeze-types": "tsc --noEmit --strict --skipLibCheck --target es2022 --module nodenext --moduleResolution nodenext --verbatimModuleSyntax l0-freeze/commit-contract.ts l0-freeze/failure-contract.ts l0-freeze/profile-contract.ts l0-freeze/command-identity-contract.ts",
154
184
  "test": "vitest run tests"
155
185
  },
156
186
  "peerDependencies": {
@@ -0,0 +1,125 @@
1
+ /**
2
+ * L-5a — artifact domain contracts (spec-v8 §9).
3
+ *
4
+ * Generic, host-independent state: StorageScope, Artifact, Binding, Release,
5
+ * File/BlobRef, Source, ACL, hostname reservation, Upload, BlobPin,
6
+ * ObjectIntent and operation idempotency. Schema types are independent of
7
+ * ORM rows; no server, driver or cluster-mesh imports; no eager storage
8
+ * connections or DDL on import. Identities are server-allocated UUIDs.
9
+ */
10
+
11
+ export const ARTIFACT_SCHEMA_VERSIONS = ['artifacts/v1'] as const;
12
+
13
+ export type ArtifactSchemaVersion = (typeof ARTIFACT_SCHEMA_VERSIONS)[number];
14
+
15
+ /** Refuse unknown schema versions (fail-closed on version skew). */
16
+ export function assertKnownSchemaVersion(version: unknown): asserts version is ArtifactSchemaVersion {
17
+ if (!ARTIFACT_SCHEMA_VERSIONS.includes(version as ArtifactSchemaVersion)) {
18
+ throw new Error(`unknown artifact schema version: ${String(version)}`);
19
+ }
20
+ }
21
+
22
+ const UUID_PATTERN =
23
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
24
+
25
+ /** True for server-allocated UUID identities. */
26
+ export function isArtifactId(value: unknown): value is string {
27
+ return typeof value === 'string' && UUID_PATTERN.test(value);
28
+ }
29
+
30
+ /** JSON value (undefined only as an absent property, never serialized). */
31
+ export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
32
+ export interface JsonObject {
33
+ [key: string]: JsonValue | undefined;
34
+ }
35
+ /**
36
+ * Content-addressed manifest: canonical bytes + SHA-256 digest, versioned
37
+ * schema. `digest` is optional: construction precedes sealing, so working
38
+ * documents carry no digest and sealed documents must verify against it.
39
+ */
40
+ export interface CanonicalManifest {
41
+ schemaVersion: string;
42
+ digest?: string;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ /** Allocate a server-side identity (UUID v4). */
47
+ export function createArtifactId(): string {
48
+ return crypto.randomUUID();
49
+ }
50
+
51
+ /** Opaque storage scope isolating physical keys from org reindexing. */
52
+ export interface StorageScope {
53
+ readonly storageScopeId: string;
54
+ readonly createdAt: string;
55
+ }
56
+
57
+ /** An artifact: one logical view across releases. */
58
+ export interface Artifact {
59
+ readonly id: string;
60
+ readonly schemaVersion: ArtifactSchemaVersion;
61
+ readonly storageScopeId: string;
62
+ readonly ownerId: string;
63
+ readonly createdAt: string;
64
+ readonly deletedAt?: string;
65
+ }
66
+
67
+ /** A stable owner-scoped binding resolving to one artifact. */
68
+ export interface Binding {
69
+ readonly binding: string;
70
+ readonly artifactId: string;
71
+ readonly ownerId: string;
72
+ }
73
+
74
+ /** An immutable release of an artifact's manifest + bytes. */
75
+ export interface Release {
76
+ readonly id: string;
77
+ readonly artifactId: string;
78
+ readonly manifestDigest: string;
79
+ readonly createdAt: string;
80
+ readonly state: 'pending' | 'ready' | 'withdrawn' | 'purged';
81
+ }
82
+
83
+ /** A content blob referenced by digest. */
84
+ export interface BlobRef {
85
+ readonly digest: string;
86
+ readonly size: number;
87
+ readonly mimeType: string;
88
+ }
89
+
90
+ /** Provenance of a release's sources. */
91
+ export type ArtifactSource =
92
+ | { readonly kind: 'inline-original' }
93
+ | { readonly kind: 'recipe'; readonly baseCommit: string; readonly recipe: string };
94
+
95
+ /** Object-level access grant. */
96
+ export interface AccessGrant {
97
+ readonly artifactId: string;
98
+ readonly subjectId: string;
99
+ readonly rights: readonly ('read' | 'source-export' | 'write')[];
100
+ }
101
+
102
+ /** A bounded upload session staging bytes before commit. */
103
+ export interface Upload {
104
+ readonly id: string;
105
+ readonly artifactId: string;
106
+ readonly prefix: string;
107
+ readonly createdAt: string;
108
+ readonly state: 'open' | 'committed' | 'aborted';
109
+ }
110
+
111
+ /** A pin retaining a blob against concurrent GC. */
112
+ export interface BlobPin {
113
+ readonly digest: string;
114
+ readonly artifactId: string;
115
+ readonly reason: 'release' | 'fork' | 'upload';
116
+ }
117
+
118
+ /** Recorded intent before every external object operation. */
119
+ export interface ObjectIntent {
120
+ readonly idempotencyKey: string;
121
+ readonly artifactId: string;
122
+ readonly operation: string;
123
+ readonly recordedAt: string;
124
+ readonly state: 'recorded' | 'applied' | 'failed';
125
+ }