@sentropic/artifacts 0.1.4 → 0.2.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 (69) hide show
  1. package/README.md +47 -0
  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/http.d.ts +43 -30
  7. package/dist/http.js +158 -507
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +2 -0
  10. package/dist/mcp-core.d.ts +32 -39
  11. package/dist/mcp-core.js +83 -244
  12. package/dist/mcp.js +5 -5
  13. package/dist/profile/contracts.d.ts +67 -0
  14. package/dist/profile/contracts.js +31 -0
  15. package/dist/profile/index.d.ts +6 -0
  16. package/dist/profile/index.js +117 -0
  17. package/dist/profile/json.d.ts +10 -0
  18. package/dist/profile/json.js +76 -0
  19. package/dist/profile/radar.d.ts +3 -0
  20. package/dist/profile/radar.js +21 -0
  21. package/dist/profile/schemas.d.ts +6 -0
  22. package/dist/profile/schemas.js +76 -0
  23. package/dist/profile/validation.d.ts +5 -0
  24. package/dist/profile/validation.js +104 -0
  25. package/dist/profile/validators.d.ts +3 -0
  26. package/dist/profile/validators.js +51 -0
  27. package/dist/server/authorized-replay.d.ts +33 -0
  28. package/dist/server/authorized-replay.js +31 -0
  29. package/dist/server/command-identity.d.ts +52 -0
  30. package/dist/server/command-identity.js +71 -0
  31. package/dist/server/commit/brands.d.ts +7 -0
  32. package/dist/server/commit/brands.js +3 -0
  33. package/dist/server/commit/coordinator.d.ts +2 -0
  34. package/dist/server/commit/coordinator.js +729 -0
  35. package/dist/server/commit/types.d.ts +382 -0
  36. package/dist/server/commit/types.js +94 -0
  37. package/dist/server/commit-contract.d.ts +4 -0
  38. package/dist/server/commit-contract.js +2 -0
  39. package/dist/server/failures.d.ts +84 -0
  40. package/dist/server/failures.js +133 -0
  41. package/dist/server/gc.d.ts +42 -1
  42. package/dist/server/gc.js +168 -5
  43. package/dist/server/idempotency.d.ts +4 -0
  44. package/dist/server/idempotency.js +14 -0
  45. package/dist/server/lifecycle.d.ts +2 -1
  46. package/dist/server/lifecycle.js +15 -1
  47. package/dist/server/object-intents.d.ts +14 -1
  48. package/dist/server/object-intents.js +31 -0
  49. package/dist/server/outcome-contract.d.ts +3 -0
  50. package/dist/server/outcome-contract.js +3 -0
  51. package/dist/server/outcome-projection.d.ts +21 -0
  52. package/dist/server/outcome-projection.js +29 -0
  53. package/dist/server/preparation-cleanup.d.ts +31 -0
  54. package/dist/server/preparation-cleanup.js +106 -0
  55. package/dist/server/publication.d.ts +78 -0
  56. package/dist/server/publication.js +263 -31
  57. package/dist/server/service.d.ts +60 -0
  58. package/dist/server/service.js +181 -0
  59. package/dist/testing.d.ts +50 -0
  60. package/dist/testing.js +86 -0
  61. package/docs/migration-l6.md +134 -0
  62. package/examples/radar-commit-setup.ts +302 -0
  63. package/l0-freeze/command-identity-contract.ts +303 -0
  64. package/l0-freeze/commit-contract.ts +421 -0
  65. package/l0-freeze/failure-contract.ts +174 -0
  66. package/l0-freeze/profile-contract.ts +219 -0
  67. package/migrations/0003_coordinator.sql +28 -0
  68. package/package.json +27 -1
  69. package/src/contracts.ts +125 -0
@@ -0,0 +1,117 @@
1
+ import { isArtifactId } from '../contracts.js';
2
+ import { ARTIFACT_CAPABILITY_NAMES, ArtifactProfileInputError, PROFILE_CONTRACT_VERSION } from './contracts.js';
3
+ import { copyJson, freezeDeep, isRecord, orderedDigest } from './json.js';
4
+ import { assertCompleteOperation, capabilitySchema } from './schemas.js';
5
+ import { matchesSchema, normalizeArguments } from './validation.js';
6
+ import { RADAR_ARTIFACTS_PROFILE } from './radar.js';
7
+ import { compileValidators } from './validators.js';
8
+ export * from './contracts.js';
9
+ export { RADAR_ARTIFACTS_PROFILE } from './radar.js';
10
+ const compiledProfiles = new WeakSet();
11
+ export function assertCompiledArtifactsProfile(value) {
12
+ if (typeof value !== 'object' || value === null || !compiledProfiles.has(value)) {
13
+ throw new Error('invalid compiled artifacts profile');
14
+ }
15
+ }
16
+ /** Compile once, before any registry, adapter, schema or integrity evidence. */
17
+ export function compileArtifactsProfile(input) {
18
+ const snapshot = copyJson(input);
19
+ const profile = { ...snapshot,
20
+ rules: snapshot.rules === undefined ? copyJson(RADAR_ARTIFACTS_PROFILE.rules) : snapshot.rules };
21
+ const allowed = ['version', 'selectedCapabilities', 'operations', 'contractRevisions', 'rules'];
22
+ if (Object.keys(profile).some((key) => !allowed.includes(key)) || profile.version !== PROFILE_CONTRACT_VERSION ||
23
+ !Array.isArray(profile.selectedCapabilities) || !Array.isArray(profile.operations))
24
+ throw new Error('invalid artifacts profile');
25
+ const requested = profile.selectedCapabilities;
26
+ if (new Set(requested).size !== requested.length ||
27
+ requested.some((name) => !ARTIFACT_CAPABILITY_NAMES.includes(name))) {
28
+ throw new Error('unknown or duplicate artifacts capability');
29
+ }
30
+ const selectedCapabilities = ARTIFACT_CAPABILITY_NAMES.filter((name) => requested.includes(name));
31
+ // Canonicalize set-like configuration while retaining command array order.
32
+ profile.selectedCapabilities = selectedCapabilities;
33
+ const operations = new Set();
34
+ for (const op of profile.operations) {
35
+ if (!op || typeof op.operation !== 'string' || operations.has(op.operation) ||
36
+ !isRecord(op.limits) || !isRecord(op.defaults) ||
37
+ !ARTIFACT_CAPABILITY_NAMES.includes(`artifacts_${op.operation}`) ||
38
+ Object.keys(op).some((key) => !['operation', 'limits', 'defaults'].includes(key)) ||
39
+ Object.values(op.limits).some((n) => typeof n !== 'number' || !Number.isSafeInteger(n) || n <= 0))
40
+ throw new Error('invalid operation profile');
41
+ assertCompleteOperation(op);
42
+ for (const [minimum, maximum] of [['bindingNameMinUnits', 'bindingNameMaxUnits'],
43
+ ['publishFilesMin', 'publishFilesMax'], ['listLimitMin', 'listLimitMax']]) {
44
+ if (op.limits[minimum] > op.limits[maximum])
45
+ throw new Error('inconsistent profile bounds');
46
+ }
47
+ operations.add(op.operation);
48
+ }
49
+ const rules = profile.rules;
50
+ const ruleKeys = ['lengthUnit', 'textEncoding', 'callerKeyNormalization', 'rejectLoneSurrogates',
51
+ 'provenance', 'keyDerivationAlgorithm', 'pathMaxBytes', 'forbiddenPathPattern', 'reservedFirstSegments', 'mimeTypes'];
52
+ if (!rules || rules.lengthUnit !== 'utf16' || rules.textEncoding !== 'utf8' || rules.callerKeyNormalization !== 'none' ||
53
+ Object.keys(rules).some((key) => !ruleKeys.includes(key)) ||
54
+ rules.rejectLoneSurrogates !== true || rules.provenance !== 'inline-original' ||
55
+ typeof rules.keyDerivationAlgorithm !== 'string' || !rules.keyDerivationAlgorithm ||
56
+ !Number.isSafeInteger(rules.pathMaxBytes) || rules.pathMaxBytes < 1 || rules.pathMaxBytes > 1024 ||
57
+ !Array.isArray(rules.mimeTypes) || !rules.mimeTypes.length || rules.mimeTypes.some((v) => typeof v !== 'string' || !v) ||
58
+ !Array.isArray(rules.reservedFirstSegments) || rules.reservedFirstSegments.some((v) => typeof v !== 'string' || !v) ||
59
+ rules.forbiddenPathPattern !== '[\\\\%?#:\\u0000-\\u001f\\u007f]')
60
+ throw new Error('invalid profile semantics');
61
+ if (!profile.contractRevisions || profile.contractRevisions.profile !== PROFILE_CONTRACT_VERSION ||
62
+ profile.contractRevisions.command !== 'artifacts-command/v1' ||
63
+ !profile.contractRevisions.authorization || !profile.contractRevisions.outcome ||
64
+ Object.values(profile.contractRevisions).some((revision) => typeof revision !== 'string' || !revision)) {
65
+ throw new Error('invalid provider contract revisions');
66
+ }
67
+ const schemas = {};
68
+ const idempotency = {};
69
+ for (const name of selectedCapabilities) {
70
+ schemas[name] = capabilitySchema(name, profile);
71
+ const op = profile.operations.find((entry) => entry.operation === name.slice(10));
72
+ if (rules.keyDerivationAlgorithm === 'radar-derive-idempotency-v1' && op.limits.idempotencyKeyMaxUnits > 256) {
73
+ throw new Error('profile/derivation key length mismatch');
74
+ }
75
+ const defaults = op.defaults;
76
+ const schema = schemas[name];
77
+ const props = schema.properties;
78
+ for (const [key, value] of Object.entries(defaults)) {
79
+ if (!Object.hasOwn(props, key) || !matchesSchema(value, props[key]))
80
+ throw new Error('profile/schema default mismatch');
81
+ }
82
+ if (name === 'artifacts_ensure' || name === 'artifacts_publish' || name === 'artifacts_commit_upload') {
83
+ idempotency[name] = { required: true, scope: 'principal' };
84
+ }
85
+ }
86
+ const assertSelected = (name) => {
87
+ if (!selectedCapabilities.includes(name))
88
+ throw new ArtifactProfileInputError(true);
89
+ };
90
+ const compiled = {
91
+ ...profile, selectedCapabilities, profile, schemas, idempotency,
92
+ get validators() { return validators; },
93
+ profileDigest: orderedDigest({ selection: selectedCapabilities, profile, schemas,
94
+ providerContractRevisions: profile.contractRevisions }),
95
+ assertSelected,
96
+ validate(name, args, envelopeKey) {
97
+ if (!selectedCapabilities.includes(name))
98
+ throw new ArtifactProfileInputError(true);
99
+ return normalizeArguments(profile, name, args, schemas[name], envelopeKey);
100
+ },
101
+ capabilityForRoute(method, path) {
102
+ let name;
103
+ if (method === 'POST' && path === '/ensure')
104
+ name = 'artifacts_ensure';
105
+ if (method === 'POST' && path === '/publish')
106
+ name = 'artifacts_publish';
107
+ if (method === 'POST' && path === '/list')
108
+ name = 'artifacts_list';
109
+ if (method === 'GET' && path.startsWith('/meta/') && isArtifactId(path.slice(6)))
110
+ name = 'artifacts_get';
111
+ return name && selectedCapabilities.includes(name) ? name : undefined;
112
+ },
113
+ };
114
+ const validators = compileValidators(compiled);
115
+ compiledProfiles.add(compiled);
116
+ return freezeDeep(compiled);
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { JsonValue } from '../contracts.js';
2
+ /** Reject before UTF-8 conversion can replace an unpaired code unit. */
3
+ export declare function assertWellFormed(value: string): void;
4
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
5
+ /** An owned JSON snapshot, refusing non-JSON, accessors and cyclic inputs. */
6
+ export declare function copyJson(value: unknown, ancestors?: Set<object>): JsonValue;
7
+ export declare function freezeDeep<T>(value: T): T;
8
+ /** Command/profile codec: objects sort recursively; arrays retain order. */
9
+ export declare function orderedJson(value: unknown): string;
10
+ export declare function orderedDigest(value: unknown): string;
@@ -0,0 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { ArtifactProfileInputError } from './contracts.js';
3
+ /** Reject before UTF-8 conversion can replace an unpaired code unit. */
4
+ export function assertWellFormed(value) {
5
+ for (let i = 0; i < value.length; i++) {
6
+ const code = value.charCodeAt(i);
7
+ if (code >= 0xd800 && code <= 0xdbff) {
8
+ const next = value.charCodeAt(++i);
9
+ if (!(next >= 0xdc00 && next <= 0xdfff))
10
+ throw new ArtifactProfileInputError();
11
+ }
12
+ else if (code >= 0xdc00 && code <= 0xdfff)
13
+ throw new ArtifactProfileInputError();
14
+ }
15
+ }
16
+ export function isRecord(value) {
17
+ return value !== null && typeof value === 'object' &&
18
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
19
+ }
20
+ /** An owned JSON snapshot, refusing non-JSON, accessors and cyclic inputs. */
21
+ export function copyJson(value, ancestors = new Set()) {
22
+ if (typeof value === 'string') {
23
+ assertWellFormed(value);
24
+ return value;
25
+ }
26
+ if (value === null || typeof value === 'boolean')
27
+ return value;
28
+ if (typeof value === 'number' && Number.isFinite(value))
29
+ return value;
30
+ if (typeof value !== 'object' || value === null || ancestors.has(value))
31
+ throw new ArtifactProfileInputError();
32
+ ancestors.add(value);
33
+ try {
34
+ if (Array.isArray(value)) {
35
+ return Array.from(value, (entry) => copyJson(entry, ancestors));
36
+ }
37
+ if (!isRecord(value) || Object.getOwnPropertySymbols(value).length)
38
+ throw new ArtifactProfileInputError();
39
+ const out = {};
40
+ for (const key of Object.keys(value)) {
41
+ assertWellFormed(key);
42
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
43
+ if (!('value' in descriptor))
44
+ throw new ArtifactProfileInputError();
45
+ Object.defineProperty(out, key, { value: copyJson(descriptor.value, ancestors), enumerable: true,
46
+ writable: true, configurable: true });
47
+ }
48
+ return out;
49
+ }
50
+ finally {
51
+ ancestors.delete(value);
52
+ }
53
+ }
54
+ export function freezeDeep(value) {
55
+ if (value !== null && typeof value === 'object') {
56
+ for (const child of Object.values(value))
57
+ freezeDeep(child);
58
+ Object.freeze(value);
59
+ }
60
+ return value;
61
+ }
62
+ /** Command/profile codec: objects sort recursively; arrays retain order. */
63
+ export function orderedJson(value) {
64
+ const canonicalize = (entry) => {
65
+ if (Array.isArray(entry))
66
+ return `[${entry.map(canonicalize).join(',')}]`;
67
+ if (entry !== null && typeof entry === 'object') {
68
+ return `{${Object.keys(entry).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(entry[key])}`).join(',')}}`;
69
+ }
70
+ return JSON.stringify(entry);
71
+ };
72
+ return canonicalize(copyJson(value));
73
+ }
74
+ export function orderedDigest(value) {
75
+ return createHash('sha256').update(orderedJson(value), 'utf8').digest('hex');
76
+ }
@@ -0,0 +1,3 @@
1
+ import type { EffectiveArtifactProfile } from './contracts.js';
2
+ /** Explicit host input, never an implicit fallback for a missing selection. */
3
+ export declare const RADAR_ARTIFACTS_PROFILE: EffectiveArtifactProfile;
@@ -0,0 +1,21 @@
1
+ import { freezeDeep } from './json.js';
2
+ /** Explicit host input, never an implicit fallback for a missing selection. */
3
+ export const RADAR_ARTIFACTS_PROFILE = freezeDeep({
4
+ version: 'artifacts-profile/v1',
5
+ selectedCapabilities: ['artifacts_ensure', 'artifacts_publish', 'artifacts_list', 'artifacts_get'],
6
+ operations: [
7
+ { operation: 'ensure', limits: { bindingNameMinUnits: 1, bindingNameMaxUnits: 128,
8
+ idempotencyKeyMaxUnits: 256 }, defaults: { parameters: {} } },
9
+ { operation: 'publish', limits: { idempotencyKeyMaxUnits: 256, publishFilesMin: 1, publishFilesMax: 32,
10
+ maxDecodedFileBytes: 262144, maxDecodedTotalBytes: 262144 }, defaults: { profile: {} } },
11
+ { operation: 'list', limits: { listLimitMin: 1, listLimitMax: 100 }, defaults: { limit: 20, offset: 0 } },
12
+ { operation: 'get', limits: {}, defaults: {} },
13
+ ],
14
+ rules: { lengthUnit: 'utf16', textEncoding: 'utf8', callerKeyNormalization: 'none',
15
+ rejectLoneSurrogates: true, provenance: 'inline-original', keyDerivationAlgorithm: 'radar-derive-idempotency-v1', pathMaxBytes: 1024,
16
+ forbiddenPathPattern: '[\\\\%?#:\\u0000-\\u001f\\u007f]',
17
+ reservedFirstSegments: ['api', '__artifacts', 'livez', 'health'],
18
+ mimeTypes: ['text/html', 'text/css', 'text/javascript', 'application/json'] },
19
+ contractRevisions: { profile: 'artifacts-profile/v1', command: 'artifacts-command/v1',
20
+ authorization: 'artifacts-authorization/v1', outcome: 'artifacts-outcome/v1' },
21
+ });
@@ -0,0 +1,6 @@
1
+ import type { ArtifactCapabilityName, ArtifactProviderProfile, EffectiveArtifactProfile, ArtifactSchema, OperationProfile } from './contracts.js';
2
+ /** Required configuration includes semantic limits absent from JSON Schema. */
3
+ export declare function assertCompleteOperation(config: OperationProfile): void;
4
+ export declare function operationProfile(profile: ArtifactProviderProfile, operation: string): OperationProfile;
5
+ /** Schemas are generated from the same limits/defaults read by normalization. */
6
+ export declare function capabilitySchema(name: ArtifactCapabilityName, profile: EffectiveArtifactProfile): ArtifactSchema;
@@ -0,0 +1,76 @@
1
+ const str = { type: 'string', minLength: 1 };
2
+ const integer = (minimum, maximum) => ({ type: 'integer', minimum,
3
+ ...(maximum === undefined ? {} : { maximum }) });
4
+ const obj = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
5
+ const id = { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' };
6
+ const primitiveRecord = { type: 'object',
7
+ additionalProperties: { type: ['string', 'number', 'boolean', 'null'] }, default: {} };
8
+ /** Required configuration includes semantic limits absent from JSON Schema. */
9
+ export function assertCompleteOperation(config) {
10
+ const required = {
11
+ ensure: { limits: ['bindingNameMinUnits', 'bindingNameMaxUnits', 'idempotencyKeyMaxUnits'],
12
+ defaults: ['parameters'] },
13
+ publish: { limits: ['idempotencyKeyMaxUnits', 'publishFilesMin', 'publishFilesMax',
14
+ 'maxDecodedFileBytes', 'maxDecodedTotalBytes'], defaults: ['profile'] },
15
+ list: { limits: ['listLimitMin', 'listLimitMax'], defaults: ['limit', 'offset'] },
16
+ commit_upload: { limits: ['idempotencyKeyMaxUnits'], defaults: ['profile'] },
17
+ };
18
+ const fields = required[config.operation];
19
+ if (fields && (fields.limits.some((key) => !Object.hasOwn(config.limits, key)) ||
20
+ fields.defaults.some((key) => !Object.hasOwn(config.defaults, key)))) {
21
+ throw new Error(`incomplete operation profile: ${config.operation}`);
22
+ }
23
+ }
24
+ export function operationProfile(profile, operation) {
25
+ const configured = profile.operations.find((entry) => entry.operation === operation);
26
+ if (!configured)
27
+ throw new Error(`missing operation profile: ${operation}`);
28
+ return configured;
29
+ }
30
+ /** Schemas are generated from the same limits/defaults read by normalization. */
31
+ export function capabilitySchema(name, profile) {
32
+ const operation = name.slice('artifacts_'.length);
33
+ const config = operationProfile(profile, operation);
34
+ const limits = config.limits;
35
+ const key = { type: 'string', minLength: 1, maxLength: limits.idempotencyKeyMaxUnits,
36
+ description: 'Nonblank by JavaScript trim(); length is UTF-16 units; original bytes retained.' };
37
+ const idOnly = obj({ artifactId: id }, ['artifactId']);
38
+ switch (operation) {
39
+ case 'ensure': return obj({ bindingName: { type: 'string', minLength: limits.bindingNameMinUnits,
40
+ maxLength: limits.bindingNameMaxUnits }, parameters: { ...obj({}), default: config.defaults.parameters },
41
+ idempotencyKey: key }, ['bindingName', 'idempotencyKey']);
42
+ case 'publish': {
43
+ const reserved = profile.rules.reservedFirstSegments.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
44
+ const path = { type: 'string', minLength: 1, maxLength: profile.rules.pathMaxBytes,
45
+ allOf: [{ not: { pattern: profile.rules.forbiddenPathPattern } },
46
+ { not: { pattern: '(^|/)(\\.\\.?|)(/|$)' } },
47
+ { not: { pattern: `^(?:${reserved})(?:/|$)` } }],
48
+ description: `At most ${profile.rules.pathMaxBytes} UTF-8 bytes; no empty/dot segments, colon or DEL.` };
49
+ const file = { ...obj({ path, mimeType: { type: 'string', enum: profile.rules.mimeTypes },
50
+ text: { type: 'string', maxLength: limits.maxDecodedFileBytes, description: 'UTF-8 decoded-byte budget applies.' },
51
+ bytes: { type: 'array', maxItems: limits.maxDecodedFileBytes, items: integer(0, 255) } }, ['path', 'mimeType']),
52
+ oneOf: [{ required: ['text'], not: { required: ['bytes'] } },
53
+ { required: ['bytes'], not: { required: ['text'] } }] };
54
+ return obj({ artifactId: id, idempotencyKey: key, files: { type: 'array', items: file,
55
+ minItems: limits.publishFilesMin, maxItems: limits.publishFilesMax },
56
+ profile: { ...primitiveRecord, default: config.defaults.profile } }, ['artifactId', 'files', 'idempotencyKey']);
57
+ }
58
+ case 'list': return obj({ limit: { ...integer(limits.listLimitMin, limits.listLimitMax), default: config.defaults.limit },
59
+ offset: { ...integer(0), default: config.defaults.offset } });
60
+ case 'get':
61
+ case 'get_source':
62
+ case 'fork':
63
+ case 'renew':
64
+ case 'delete':
65
+ case 'restore': return idOnly;
66
+ case 'get_template': return obj({ templateId: str }, ['templateId']);
67
+ case 'get_api_guide': return obj({ version: str });
68
+ case 'create_upload': return obj({ artifactId: id, mimeType: str, maxSize: integer(1) }, ['artifactId', 'mimeType']);
69
+ case 'commit_upload': return obj({ artifactId: id, uploadId: str, path: str, mimeType: str,
70
+ digest: str, size: integer(0), profile: primitiveRecord, idempotencyKey: key }, ['artifactId', 'uploadId', 'path', 'mimeType', 'digest', 'size', 'idempotencyKey']);
71
+ case 'set_access': return obj({ artifactId: id, action: { type: 'string', enum: ['grant', 'revoke', 'set-public'] },
72
+ principalId: str, grant: { type: 'string', enum: ['read', 'source'] }, isPublic: { type: 'boolean' } }, ['artifactId', 'action']);
73
+ case 'promote': return obj({ artifactId: id, releaseId: id, expectedCurrent: id }, ['artifactId', 'releaseId', 'expectedCurrent']);
74
+ default: throw new Error(`unsupported operation profile: ${operation}`);
75
+ }
76
+ }
@@ -0,0 +1,5 @@
1
+ import { type JsonValue } from '../contracts.js';
2
+ import { type EffectiveArtifactProfile, type ArtifactSchema } from './contracts.js';
3
+ /** The generated schema vocabulary, evaluated without a platform dependency. */
4
+ export declare function matchesSchema(value: unknown, schema: ArtifactSchema): boolean;
5
+ export declare function normalizeArguments(profile: EffectiveArtifactProfile, name: string, input: unknown, schema: ArtifactSchema, envelopeKey?: string): Readonly<Record<string, JsonValue>>;
@@ -0,0 +1,104 @@
1
+ import { isArtifactId } from '../contracts.js';
2
+ import { ArtifactProfileInputError } from './contracts.js';
3
+ import { copyJson, freezeDeep, isRecord } from './json.js';
4
+ import { operationProfile } from './schemas.js';
5
+ function requireInput(condition) {
6
+ if (!condition)
7
+ throw new ArtifactProfileInputError();
8
+ }
9
+ /** The generated schema vocabulary, evaluated without a platform dependency. */
10
+ export function matchesSchema(value, schema) {
11
+ const type = schema.type;
12
+ if (Array.isArray(type))
13
+ return type.some((entry) => matchesSchema(value, { ...schema, type: entry }));
14
+ if (type === 'object' && !isRecord(value))
15
+ return false;
16
+ if (type === 'array' && !Array.isArray(value))
17
+ return false;
18
+ if (type === 'string' && typeof value !== 'string')
19
+ return false;
20
+ if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value)))
21
+ return false;
22
+ if (type === 'integer' && !Number.isSafeInteger(value))
23
+ return false;
24
+ if (type === 'boolean' && typeof value !== 'boolean')
25
+ return false;
26
+ if (type === 'null' && value !== null)
27
+ return false;
28
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value))
29
+ return false;
30
+ if (schema.not && matchesSchema(value, schema.not))
31
+ return false;
32
+ if (Array.isArray(schema.allOf) && !schema.allOf.every((s) => matchesSchema(value, s)))
33
+ return false;
34
+ if (Array.isArray(schema.oneOf) && schema.oneOf.filter((s) => matchesSchema(value, s)).length !== 1)
35
+ return false;
36
+ if (typeof value === 'number' &&
37
+ ((typeof schema.minimum === 'number' && value < schema.minimum) ||
38
+ (typeof schema.maximum === 'number' && value > schema.maximum)))
39
+ return false;
40
+ if (typeof value === 'string') {
41
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength)
42
+ return false;
43
+ if (typeof schema.maxLength === 'number' && value.length > schema.maxLength)
44
+ return false;
45
+ if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern).test(value))
46
+ return false;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems)
50
+ return false;
51
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems)
52
+ return false;
53
+ if (schema.items && !value.every((v) => matchesSchema(v, schema.items)))
54
+ return false;
55
+ }
56
+ if (isRecord(value)) {
57
+ if (Array.isArray(schema.required) && schema.required.some((key) => !Object.hasOwn(value, key)))
58
+ return false;
59
+ const properties = (schema.properties ?? {});
60
+ for (const [key, entry] of Object.entries(value)) {
61
+ if (schema.propertyNames && !matchesSchema(key, schema.propertyNames))
62
+ return false;
63
+ if (Object.hasOwn(properties, key)) {
64
+ if (!matchesSchema(entry, properties[key]))
65
+ return false;
66
+ }
67
+ else if (schema.additionalProperties === false)
68
+ return false;
69
+ else if (isRecord(schema.additionalProperties) && !matchesSchema(entry, schema.additionalProperties))
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ export function normalizeArguments(profile, name, input, schema, envelopeKey) {
76
+ const args = copyJson(input);
77
+ requireInput(isRecord(args) && matchesSchema(args, schema));
78
+ const operation = name.slice('artifacts_'.length);
79
+ const { defaults, limits } = operationProfile(profile, operation);
80
+ const normalized = { ...copyJson(defaults), ...args };
81
+ if (operation === 'ensure' || operation === 'publish' || operation === 'commit_upload') {
82
+ const key = args.idempotencyKey;
83
+ requireInput(typeof key === 'string' && key.trim().length > 0 && key.length <= limits.idempotencyKeyMaxUnits);
84
+ requireInput(envelopeKey === undefined || envelopeKey === key);
85
+ }
86
+ else
87
+ requireInput(envelopeKey === undefined);
88
+ if (Object.hasOwn(args, 'artifactId'))
89
+ requireInput(isArtifactId(args.artifactId));
90
+ if (operation === 'publish') {
91
+ const paths = new Set();
92
+ let total = 0;
93
+ normalized.files = args.files.map((file) => {
94
+ const path = file.path;
95
+ requireInput(!paths.has(path) && Buffer.byteLength(path, 'utf8') <= profile.rules.pathMaxBytes);
96
+ paths.add(path);
97
+ const bytes = Object.hasOwn(file, 'text') ? Array.from(Buffer.from(file.text, 'utf8')) : file.bytes;
98
+ total += bytes.length;
99
+ requireInput(bytes.length <= limits.maxDecodedFileBytes && total <= limits.maxDecodedTotalBytes);
100
+ return { path, mimeType: file.mimeType, bytes };
101
+ });
102
+ }
103
+ return freezeDeep(normalized);
104
+ }
@@ -0,0 +1,3 @@
1
+ import { type CompiledArtifactsProfile, type CompiledValidators } from './contracts.js';
2
+ /** Validators close over the same immutable schemas, limits and command compiler. */
3
+ export declare function compileValidators(profile: CompiledArtifactsProfile): CompiledValidators;
@@ -0,0 +1,51 @@
1
+ import { ArtifactProfileInputError } from './contracts.js';
2
+ import { copyJson, isRecord } from './json.js';
3
+ import { matchesSchema } from './validation.js';
4
+ import { createArtifactCommand } from '../server/command-identity.js';
5
+ /** Validators close over the same immutable schemas, limits and command compiler. */
6
+ export function compileValidators(profile) {
7
+ const field = (schema, key) => schema?.properties?.[key];
8
+ const accepts = (check) => {
9
+ try {
10
+ return check();
11
+ }
12
+ catch (error) {
13
+ if (error instanceof ArtifactProfileInputError)
14
+ return false;
15
+ throw error;
16
+ }
17
+ };
18
+ const keySchemas = Object.values(profile.schemas).flatMap((schema) => {
19
+ const key = field(schema, 'idempotencyKey');
20
+ return key ? [key] : [];
21
+ });
22
+ return {
23
+ validateBindingName: (value) => accepts(() => {
24
+ const schema = field(profile.schemas.artifacts_ensure, 'bindingName');
25
+ return !!schema && matchesSchema(copyJson(value), schema);
26
+ }),
27
+ validateCallerKey: (value) => accepts(() => typeof value === 'string' && !!value.trim() &&
28
+ keySchemas.length > 0 && keySchemas.every((schema) => matchesSchema(copyJson(value), schema))),
29
+ validateFileEntry: (entry) => accepts(() => {
30
+ const schema = field(profile.schemas.artifacts_publish, 'files')?.items;
31
+ const file = copyJson(entry);
32
+ if (!schema || !isRecord(file) || !matchesSchema(file, schema))
33
+ return false;
34
+ const limits = profile.operations.find((op) => op.operation === 'publish').limits;
35
+ const size = Object.hasOwn(file, 'text') ? Buffer.byteLength(file.text, 'utf8') :
36
+ file.bytes.length;
37
+ return Buffer.byteLength(file.path, 'utf8') <= profile.rules.pathMaxBytes &&
38
+ size <= limits.maxDecodedFileBytes;
39
+ }),
40
+ normalizeCommand(value) {
41
+ if (!isRecord(value) || typeof value.capability !== 'string' || !isRecord(value.authority) ||
42
+ !isRecord(value.derivation) || typeof value.derivation.derive !== 'function' ||
43
+ (value.envelopeKey !== undefined && typeof value.envelopeKey !== 'string') ||
44
+ Object.keys(value).some((key) => !['capability', 'args', 'authority', 'derivation', 'envelopeKey'].includes(key))) {
45
+ throw new ArtifactProfileInputError();
46
+ }
47
+ const input = value;
48
+ return createArtifactCommand(profile, input.capability, input.args, input.authority, input.derivation, input.envelopeKey);
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * L5 (immo #861) — guarded ensure replay (spec r2 §§3.1 A5/A7, 3.2).
3
+ *
4
+ * Ensure-domain adapter over the L3 finalization coordinator: replay goes
5
+ * through replayGuarded with the request-bound guard, never through a raw
6
+ * receipt read. A stored success replays only while currently authorized,
7
+ * without a quota recharge (the coordinator runs the pre-quota lock groups
8
+ * on the replay path). Unknown legacy codecs propagate as quarantine for
9
+ * the L6 migration projection.
10
+ */
11
+ import type { ArtifactCommitCoordinator, FinalizeRequest, FinalizeResult, InternalIdempotencyKey, StoredCommitReceipt } from './commit/types.js';
12
+ import type { ArtifactInvocationContext } from './commit/types.js';
13
+ /** Durable ensure receipt carried by the success record. */
14
+ export interface EnsureReceipt {
15
+ readonly artifactId: string;
16
+ readonly created: boolean;
17
+ }
18
+ /** Guarded replay input: durable key plus the effective command hash. */
19
+ export interface GuardedReplayInput {
20
+ readonly invocation: ArtifactInvocationContext;
21
+ readonly internalKey: InternalIdempotencyKey;
22
+ readonly commandHash: string;
23
+ readonly policyRevision: string;
24
+ readonly locks?: FinalizeRequest['locks'];
25
+ readonly artifactId?: string;
26
+ readonly bindingName?: string;
27
+ readonly validateReplayReceipt?: FinalizeRequest['validateReplayReceipt'];
28
+ readonly noteAbandonedAttempt?: FinalizeRequest['noteAbandonedAttempt'];
29
+ }
30
+ /** Replay one ensure through the coordinator with the request-bound guard. */
31
+ export declare function replayEnsureGuarded(coordinator: ArtifactCommitCoordinator, input: GuardedReplayInput): Promise<FinalizeResult>;
32
+ /** Project a stored ensure receipt; rejects rows that are not ensure receipts. */
33
+ export declare function projectEnsureReceipt(receipt: StoredCommitReceipt): EnsureReceipt;
@@ -0,0 +1,31 @@
1
+ /** Replay one ensure through the coordinator with the request-bound guard. */
2
+ export async function replayEnsureGuarded(coordinator, input) {
3
+ return coordinator.replayPrepared({
4
+ scopeId: input.invocation.storageScopeId,
5
+ internalKey: input.internalKey,
6
+ commandHash: input.commandHash,
7
+ codecVersion: 'artifacts-command/v1',
8
+ invocationId: input.invocation.invocationId,
9
+ actorId: input.invocation.actorId,
10
+ operation: 'ensure',
11
+ // The binding lock resolves the target before the coordinator builds its guard context.
12
+ get artifactId() { return input.artifactId ?? null; },
13
+ bindingName: input.bindingName ?? null,
14
+ policyRevision: input.policyRevision,
15
+ locks: input.locks,
16
+ guard: input.invocation.authorizeCommit,
17
+ receiptData: undefined,
18
+ validateReplayReceipt: input.validateReplayReceipt,
19
+ noteAbandonedAttempt: input.noteAbandonedAttempt,
20
+ attemptId: input.invocation.invocationId,
21
+ });
22
+ }
23
+ /** Project a stored ensure receipt; rejects rows that are not ensure receipts. */
24
+ export function projectEnsureReceipt(receipt) {
25
+ const data = receipt.receipt;
26
+ if (typeof data?.artifactId !== 'string' || data.artifactId.length === 0 ||
27
+ typeof data?.created !== 'boolean') {
28
+ throw new Error('ensure receipt mismatch');
29
+ }
30
+ return { artifactId: data.artifactId, created: data.created };
31
+ }
@@ -0,0 +1,52 @@
1
+ import { type JsonValue } from '../contracts.js';
2
+ import { type CompiledArtifactsProfile } from '../profile/index.js';
3
+ import { orderedDigest } from '../profile/json.js';
4
+ export declare const COMMAND_CODEC_VERSION: "artifacts-command/v1";
5
+ export type CallerIdempotencyKey = string & {
6
+ readonly __brand: 'caller-key';
7
+ };
8
+ export type InternalIdempotencyKey = string & {
9
+ readonly __brand: 'internal-key';
10
+ };
11
+ export interface CommandIdentity {
12
+ readonly internalKey: InternalIdempotencyKey;
13
+ readonly commandHash: string;
14
+ readonly codecVersion: typeof COMMAND_CODEC_VERSION;
15
+ }
16
+ export interface EffectiveArtifactCommand {
17
+ readonly operation: 'ensure' | 'publish';
18
+ readonly scopeBinding: string;
19
+ readonly payload: Readonly<Record<string, JsonValue>>;
20
+ readonly provenance: {
21
+ readonly kind: 'inline-original';
22
+ };
23
+ readonly identity: CommandIdentity;
24
+ }
25
+ export interface ArtifactCommandAuthority {
26
+ readonly storageScopeId: string;
27
+ readonly actorId: string;
28
+ }
29
+ export interface ArtifactKeyDerivationBinding {
30
+ readonly algorithm: string;
31
+ derive(input: ArtifactCommandAuthority & {
32
+ readonly operation: 'ensure' | 'publish';
33
+ readonly objectId: string;
34
+ readonly callerKey: CallerIdempotencyKey;
35
+ }): InternalIdempotencyKey;
36
+ }
37
+ /** Trusted server input for the compiled command normalizer, never tool arguments. */
38
+ export interface ArtifactCommandInput {
39
+ readonly capability: string;
40
+ readonly args: unknown;
41
+ readonly authority: ArtifactCommandAuthority;
42
+ readonly derivation: ArtifactKeyDerivationBinding;
43
+ readonly envelopeKey?: string;
44
+ }
45
+ /** Preserve Radar's durable bytes only within an explicitly qualified domain. */
46
+ export declare function createRadarKeyDerivation(config: {
47
+ readonly storageScopeId: string;
48
+ readonly actorIds: readonly string[];
49
+ }): ArtifactKeyDerivationBinding;
50
+ export declare const commandDigest: typeof orderedDigest;
51
+ /** No caller hash, actor, scope, transport or audit identifier enters this API through arguments. */
52
+ export declare function createArtifactCommand(profile: CompiledArtifactsProfile, name: string, args: unknown, authority: ArtifactCommandAuthority, derivation: ArtifactKeyDerivationBinding, envelopeKey?: string): EffectiveArtifactCommand;