@sentropic/artifacts 0.2.0 → 0.2.2
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 +30 -0
- package/dist/adapters/s3.js +2 -7
- package/dist/http.d.ts +3 -1
- package/dist/http.js +3 -13
- package/dist/mcp-core.d.ts +5 -2
- package/dist/mcp-core.js +5 -5
- package/dist/server/blob-prefix.d.ts +2 -0
- package/dist/server/blob-prefix.js +14 -0
- package/dist/server/commit/coordinator.js +9 -9
- package/dist/server/commit/types.d.ts +3 -0
- package/dist/server/publication.d.ts +3 -1
- package/dist/server/publication.js +12 -17
- package/docs/migration-l6.md +6 -1
- package/examples/radar-commit-setup.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @sentropic/artifacts — generic artifact contracts (L-5a)
|
|
2
2
|
|
|
3
|
+
Version 0.2.1 fixes host commit authorization being masked by the built-in
|
|
4
|
+
publication guard and wires the optional trusted `mapHostError` dependency.
|
|
5
|
+
Publication retains its lifecycle, ACL and policy revision checks; the host
|
|
6
|
+
`authorizeCommit` binding decides account approval and its sanitized refusal code.
|
|
7
|
+
Custom services using `createGuardedPublicationService` may pass the request-bound
|
|
8
|
+
`authorizeCommit` in `publishFinalized(input)`. It runs after
|
|
9
|
+
lifecycle, ACL and policy revision checks. When omitted, publication keeps the
|
|
10
|
+
default `participant.checkApproval` check and denies unapproved or expired commits.
|
|
11
|
+
|
|
12
|
+
Bind the same mapper on `createHttpArtifactApp`, `createArtifactsToolExecutor`
|
|
13
|
+
(or `createArtifactsAdapter`) and `createArtifactCommitCoordinator` so host wrapper
|
|
14
|
+
errors are classified at their originating layer. For example:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { ArtifactOperationError, type ArtifactHostFailureMapper }
|
|
18
|
+
from '@sentropic/artifacts/server/outcome-contract';
|
|
19
|
+
|
|
20
|
+
const mapHostError: ArtifactHostFailureMapper = cause => {
|
|
21
|
+
const code = (cause as { code?: string } | null)?.code;
|
|
22
|
+
return code === 'artifact_s3_timeout' || code === 'artifact_connection_lost'
|
|
23
|
+
? new ArtifactOperationError('provider_unavailable', { cause }) : undefined;
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pass `{ ...existingOptions, mapHostError }` to each construction. The packed
|
|
28
|
+
`examples/radar-commit-setup.ts` also accepts this option. Unrecognized failures
|
|
29
|
+
keep their existing classification; private causes never enter HTTP/MCP results.
|
|
30
|
+
Frozen contracts, command identity and selected tools remain unchanged.
|
|
31
|
+
|
|
32
|
+
|
|
3
33
|
Generic, host-independent artifact domain state for the Sentropic artifacts
|
|
4
34
|
provider (`sentropic.artifacts`, spec-v8 §9), covering L-5a–5e. Version
|
|
5
35
|
`0.1.2` adds UTF-8 text publication, exposes storage adapters and ships SQL migrations.
|
package/dist/adapters/s3.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* host and injected — this module never reads env or creates connections.
|
|
11
11
|
*/
|
|
12
12
|
import { DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, } from '@aws-sdk/client-s3';
|
|
13
|
+
import { assertBlobPrefix } from '../server/blob-prefix.js';
|
|
13
14
|
const BLOB_KEY_PATTERN = /^artifacts\/[A-Za-z0-9_-]+\/blobs\/sha256\/[0-9a-f]{64}$/;
|
|
14
15
|
/** Reject anything but digest-addressed blob keys, before any I/O. */
|
|
15
16
|
function assertBlobKey(key) {
|
|
@@ -17,12 +18,6 @@ function assertBlobKey(key) {
|
|
|
17
18
|
throw new Error(`invalid blob key (must be artifacts/<scope>/blobs/sha256/<hex>): ${key}`);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
/** Reject empty or escaping prefixes before any I/O. */
|
|
21
|
-
function assertPrefix(prefix) {
|
|
22
|
-
if (prefix.length === 0 || prefix.includes('..')) {
|
|
23
|
-
throw new Error(`invalid blob prefix: ${prefix}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
21
|
/** Create an S3-backed blob store over an injected client (no env reads). */
|
|
27
22
|
export function createS3BlobStore(options) {
|
|
28
23
|
return {
|
|
@@ -49,7 +44,7 @@ export function createS3BlobStore(options) {
|
|
|
49
44
|
}
|
|
50
45
|
},
|
|
51
46
|
async listKeys(prefix) {
|
|
52
|
-
|
|
47
|
+
assertBlobPrefix(prefix);
|
|
53
48
|
const keys = [];
|
|
54
49
|
let token;
|
|
55
50
|
do {
|
package/dist/http.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type GuardedPublicationDeps, type PublicationService } from './server/p
|
|
|
6
6
|
import type { AclStore } from './server/access.js';
|
|
7
7
|
import type { LifecycleService } from './server/lifecycle.js';
|
|
8
8
|
import { type ArtifactInvocationContext, type ArtifactCommitCoordinator } from './server/commit/types.js';
|
|
9
|
-
import { type ArtifactOutcome } from './server/failures.js';
|
|
9
|
+
import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
|
|
10
10
|
export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
11
11
|
export interface HttpRequest {
|
|
12
12
|
method: string;
|
|
@@ -27,6 +27,8 @@ export declare function errorBody(status: number): {
|
|
|
27
27
|
error: string;
|
|
28
28
|
};
|
|
29
29
|
export interface HttpAppDeps {
|
|
30
|
+
/** Trusted mapping of host wrapper errors; causes remain private. Also bind on the coordinator. */
|
|
31
|
+
mapHostError?: ArtifactHostFailureMapper;
|
|
30
32
|
profile: CompiledArtifactsProfile;
|
|
31
33
|
coordinator: ArtifactCommitCoordinator;
|
|
32
34
|
derivation: ArtifactKeyDerivationBinding;
|
package/dist/http.js
CHANGED
|
@@ -110,25 +110,15 @@ export function createHttpArtifactApp(deps) {
|
|
|
110
110
|
const acl = await visible(artifactId, invocation);
|
|
111
111
|
if (acl.ownerId !== invocation.actorId)
|
|
112
112
|
throw new ArtifactOperationError('access_denied');
|
|
113
|
-
// Both finalization and fresh replay combine provider checks with this request's host guard.
|
|
114
|
-
const bindGuard = (request) => ({ ...request,
|
|
115
|
-
guard: async (context, transaction) => {
|
|
116
|
-
const decision = await request.guard(context, transaction);
|
|
117
|
-
return decision.allowed ? invocation.authorizeCommit(context, transaction) : decision;
|
|
118
|
-
} });
|
|
119
|
-
const coordinator = {
|
|
120
|
-
...deps.coordinator,
|
|
121
|
-
finalizePrepared: (request) => deps.coordinator.finalizePrepared(bindGuard(request)),
|
|
122
|
-
replayPrepared: (request) => deps.coordinator.replayPrepared(bindGuard(request)),
|
|
123
|
-
};
|
|
124
113
|
const limits = profile.operations.find((entry) => entry.operation === 'publish').limits;
|
|
125
|
-
const publication = createGuardedPublicationService({ ...deps.publication, coordinator,
|
|
114
|
+
const publication = createGuardedPublicationService({ ...deps.publication, coordinator: deps.coordinator,
|
|
126
115
|
limits: { maxFiles: limits.publishFilesMax, maxFileBytes: limits.maxDecodedFileBytes,
|
|
127
116
|
maxTotalBytes: limits.maxDecodedTotalBytes } });
|
|
128
117
|
const outcome = await publication.publishFinalized({
|
|
129
118
|
ownerId: acl.ownerId, storageScopeId: invocation.storageScopeId, artifactId,
|
|
130
119
|
identity: command.identity, actorId: invocation.actorId, invocationId: invocation.invocationId,
|
|
131
120
|
attemptId: invocation.invocationId, policyRevision: await deps.ensureHost.readPolicyRevision(),
|
|
121
|
+
authorizeCommit: invocation.authorizeCommit,
|
|
132
122
|
source: { kind: 'inline-original' }, profile: normalized.profile,
|
|
133
123
|
files: normalized.files.map((file) => ({
|
|
134
124
|
path: file.path, mimeType: file.mimeType, bytes: Uint8Array.from(file.bytes),
|
|
@@ -178,7 +168,7 @@ export function createHttpArtifactApp(deps) {
|
|
|
178
168
|
catch (cause) {
|
|
179
169
|
const failure = classifyArtifactFailure(cause, { invocationId: invocation?.invocationId ?? '',
|
|
180
170
|
operation, phase, commitState }, (error) => error instanceof ArtifactProfileInputError
|
|
181
|
-
? new ArtifactOperationError(error.category) :
|
|
171
|
+
? new ArtifactOperationError(error.category) : deps.mapHostError?.(error));
|
|
182
172
|
if (cause instanceof CommitQuarantineError) {
|
|
183
173
|
const migration = { ...failure, kind: 'operational', category: 'provider_unavailable',
|
|
184
174
|
variant: 'legacy_quarantine', httpStatus: 503, publicCode: 'capability_unavailable', retriable: false };
|
package/dist/mcp-core.d.ts
CHANGED
|
@@ -4,13 +4,13 @@ import type { ProviderParameterSchema } from '@sentropic/mcp-platform/registry';
|
|
|
4
4
|
import type { ArtifactApp, HttpResponse } from './http.js';
|
|
5
5
|
import { type CompiledArtifactsProfile } from './profile/index.js';
|
|
6
6
|
import type { ArtifactInvocationContext } from './server/commit/types.js';
|
|
7
|
-
import { type ArtifactOutcome } from './server/failures.js';
|
|
7
|
+
import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
|
|
8
8
|
export * from './profile/index.js';
|
|
9
9
|
export { createRadarKeyDerivation } from './server/command-identity.js';
|
|
10
10
|
export type { ArtifactKeyDerivationBinding } from './server/command-identity.js';
|
|
11
11
|
export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
|
|
12
12
|
export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
|
|
13
|
-
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.
|
|
13
|
+
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.2";
|
|
14
14
|
export declare const ARTIFACTS_TOOL_NAMES: readonly ["artifacts_get_template", "artifacts_get_api_guide", "artifacts_ensure", "artifacts_publish", "artifacts_create_upload", "artifacts_commit_upload", "artifacts_list", "artifacts_get", "artifacts_get_source", "artifacts_fork", "artifacts_set_access", "artifacts_promote", "artifacts_renew", "artifacts_delete", "artifacts_restore"];
|
|
15
15
|
export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
|
|
16
16
|
export declare const ARTIFACTS_TOOL_SPECS: {
|
|
@@ -40,11 +40,14 @@ export interface ArtifactsInvokeInput {
|
|
|
40
40
|
export declare function createArtifactsToolExecutor(deps: {
|
|
41
41
|
app: ArtifactsApp;
|
|
42
42
|
profile: CompiledArtifactsProfile;
|
|
43
|
+
mapHostError?: ArtifactHostFailureMapper;
|
|
43
44
|
}): {
|
|
44
45
|
execute: (input: ArtifactsInvokeInput) => Promise<ArtifactOutcome<HttpResponse>>;
|
|
45
46
|
invoke(input: ArtifactsInvokeInput): Promise<HttpResponse>;
|
|
46
47
|
};
|
|
47
48
|
export interface ArtifactsAdapterOptions extends ArtifactsManifestOptions {
|
|
49
|
+
/** Trusted mapping for failures in the MCP host binding. */
|
|
50
|
+
mapHostError?: ArtifactHostFailureMapper;
|
|
48
51
|
app: ArtifactsApp;
|
|
49
52
|
/** Bind verified principal/scope and request authorization in trusted host code. */
|
|
50
53
|
createInvocation(request: AppToolInvocation): ArtifactInvocationContext | Promise<ArtifactInvocationContext>;
|
package/dist/mcp-core.js
CHANGED
|
@@ -6,7 +6,7 @@ export * from './profile/index.js';
|
|
|
6
6
|
export { createRadarKeyDerivation } from './server/command-identity.js';
|
|
7
7
|
export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
|
|
8
8
|
export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
|
|
9
|
-
export const ARTIFACTS_PROVIDER_VERSION = '0.2.
|
|
9
|
+
export const ARTIFACTS_PROVIDER_VERSION = '0.2.2';
|
|
10
10
|
export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
|
|
11
11
|
export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
|
|
12
12
|
description: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
|
|
@@ -36,9 +36,9 @@ export function createArtifactsManifest(options) {
|
|
|
36
36
|
audit: { eventKinds: ['invoke'], artifactsProfileDigest: profile.profileDigest }, durability: {},
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
function rejected(cause, invocationId, operation) {
|
|
39
|
+
function rejected(cause, invocationId, operation, mapHostError) {
|
|
40
40
|
return { ok: false, failure: classifyArtifactFailure(cause, { invocationId, operation,
|
|
41
|
-
phase: cause instanceof ArtifactProfileInputError ? cause.phase : 'entry', commitState: 'not_committed' }, (error) => error instanceof ArtifactProfileInputError ? new ArtifactOperationError(error.category) :
|
|
41
|
+
phase: cause instanceof ArtifactProfileInputError ? cause.phase : 'entry', commitState: 'not_committed' }, (error) => error instanceof ArtifactProfileInputError ? new ArtifactOperationError(error.category) : mapHostError?.(error)) };
|
|
42
42
|
}
|
|
43
43
|
export function createArtifactsToolExecutor(deps) {
|
|
44
44
|
assertArtifactsTransportProfile(deps.profile);
|
|
@@ -70,7 +70,7 @@ export function createArtifactsToolExecutor(deps) {
|
|
|
70
70
|
storageScopeId: input.invocation.storageScopeId, idempotencyKey: input.idempotencyKey }, input.invocation);
|
|
71
71
|
}
|
|
72
72
|
catch (cause) {
|
|
73
|
-
return rejected(cause, input.invocation?.invocationId ?? '', input.tool);
|
|
73
|
+
return rejected(cause, input.invocation?.invocationId ?? '', input.tool, deps.mapHostError);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
return { execute, async invoke(input) {
|
|
@@ -100,7 +100,7 @@ export function createArtifactsAdapter(options) {
|
|
|
100
100
|
invocation, idempotencyKey: request.idempotencyKey });
|
|
101
101
|
}
|
|
102
102
|
catch (cause) {
|
|
103
|
-
outcome = rejected(cause, request.ctx.requestId, request.capabilityRef);
|
|
103
|
+
outcome = rejected(cause, request.ctx.requestId, request.capabilityRef, options.mapHostError);
|
|
104
104
|
}
|
|
105
105
|
return projectArtifactMcpOutcome(outcome, request.ctx.auditId);
|
|
106
106
|
},
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ArtifactOperationError } from './failures.js';
|
|
2
|
+
class InvalidBlobPrefixError extends ArtifactOperationError {
|
|
3
|
+
code = 'invalid_blob_prefix';
|
|
4
|
+
constructor(prefix) {
|
|
5
|
+
super('invalid_request');
|
|
6
|
+
this.message = `invalid blob prefix: ${prefix}`;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Reject empty or escaping prefixes before enumeration or external I/O. */
|
|
10
|
+
export function assertBlobPrefix(prefix) {
|
|
11
|
+
if (prefix.length === 0 || prefix.includes('..')) {
|
|
12
|
+
throw new InvalidBlobPrefixError(prefix);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -59,10 +59,10 @@ const isConnectionLoss = (error) => {
|
|
|
59
59
|
const LOCK_TIMEOUT_CODE = '55P03';
|
|
60
60
|
/** Serialization/deadlock/cancel outcomes: retriable unavailability (r2 E3/P2-2). */
|
|
61
61
|
const RETRIABLE_SQLSTATES = new Set(['40001', '40P01', '57014']);
|
|
62
|
-
const classifyOperational = (request, phase, commitState, error) => {
|
|
62
|
+
const classifyOperational = (request, phase, commitState, error, mapHostError) => {
|
|
63
63
|
const classified = classifyArtifactFailure(error, {
|
|
64
64
|
invocationId: request.invocationId, operation: request.operation, phase, commitState,
|
|
65
|
-
});
|
|
65
|
+
}, mapHostError);
|
|
66
66
|
if (classified.kind === 'operational')
|
|
67
67
|
return classified;
|
|
68
68
|
return failure('provider_error', request, phase, commitState, error, { variant: 'unclassified' });
|
|
@@ -227,7 +227,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
227
227
|
const code = typeof error === 'object' && error !== null
|
|
228
228
|
? error.code : undefined;
|
|
229
229
|
if (settleError === undefined && typeof code === 'string' && RETRIABLE_SQLSTATES.has(code)) {
|
|
230
|
-
return { ok: false, failure: classifyOperational(request, 'commit', 'not_committed', error) };
|
|
230
|
+
return { ok: false, failure: classifyOperational(request, 'commit', 'not_committed', error, options.mapHostError) };
|
|
231
231
|
}
|
|
232
232
|
return unknownOutcome(request, 'commit', settleError === undefined ? error : { commit: error, settlement: settleError });
|
|
233
233
|
};
|
|
@@ -374,7 +374,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
374
374
|
await abandon(request);
|
|
375
375
|
return {
|
|
376
376
|
ok: false,
|
|
377
|
-
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
377
|
+
failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
378
378
|
};
|
|
379
379
|
}
|
|
380
380
|
const settle = trackSettlement(connection);
|
|
@@ -423,7 +423,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
423
423
|
}
|
|
424
424
|
return {
|
|
425
425
|
ok: false,
|
|
426
|
-
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
426
|
+
failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
427
427
|
};
|
|
428
428
|
}
|
|
429
429
|
};
|
|
@@ -484,7 +484,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
484
484
|
ok: false,
|
|
485
485
|
failure: isConnectionLoss(error)
|
|
486
486
|
? failure('provider_unavailable', request, phase, 'not_committed', error)
|
|
487
|
-
: classifyOperational(request, phase, 'not_committed', error),
|
|
487
|
+
: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
const active = connection;
|
|
@@ -613,7 +613,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
613
613
|
}
|
|
614
614
|
return {
|
|
615
615
|
ok: false,
|
|
616
|
-
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
616
|
+
failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
617
617
|
};
|
|
618
618
|
}
|
|
619
619
|
},
|
|
@@ -630,7 +630,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
630
630
|
await abandon(request);
|
|
631
631
|
return {
|
|
632
632
|
ok: false,
|
|
633
|
-
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
633
|
+
failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
636
|
const active = connection;
|
|
@@ -720,7 +720,7 @@ export function createArtifactCommitCoordinator(options) {
|
|
|
720
720
|
}
|
|
721
721
|
return {
|
|
722
722
|
ok: false,
|
|
723
|
-
failure: classifyOperational(request, phase, 'not_committed', error),
|
|
723
|
+
failure: classifyOperational(request, phase, 'not_committed', error, options.mapHostError),
|
|
724
724
|
};
|
|
725
725
|
}
|
|
726
726
|
},
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { CommitArrivalObserver, CommitBarrierObserver, CommitBarrierPoint } from '../../testing.js';
|
|
15
15
|
import type { commitFenceBrand, commitTransactionBrand } from './brands.js';
|
|
16
|
+
import type { ArtifactHostFailureMapper } from '../failures.js';
|
|
16
17
|
/** Version of the frozen command-codec shape (r2 C6). */
|
|
17
18
|
export declare const COMMAND_CODEC_VERSION: "artifacts-command/v1";
|
|
18
19
|
/** Server-derived internal key: distinct from any caller key. */
|
|
@@ -302,6 +303,8 @@ export type ArtifactOutcome<T> = {
|
|
|
302
303
|
export type FinalizeResult = ArtifactOutcome<StoredCommitReceipt>;
|
|
303
304
|
/** Coordinator construction options. */
|
|
304
305
|
export interface CommitCoordinatorOptions {
|
|
306
|
+
/** Trusted host wrapper classification for failures inside the atomic unit. */
|
|
307
|
+
readonly mapHostError?: ArtifactHostFailureMapper;
|
|
305
308
|
readonly executor: CommitExecutor;
|
|
306
309
|
readonly policy: CommitPolicyParticipantFactory;
|
|
307
310
|
/** Trusted domain adapters; every call resolves fresh state, including replay. */
|
|
@@ -7,7 +7,7 @@ import type { ArtifactAcl } from './access.js';
|
|
|
7
7
|
import type { ArtifactState } from './lifecycle.js';
|
|
8
8
|
import { type RefcountStore } from './gc.js';
|
|
9
9
|
import type { CommandIdentity } from './command-identity.js';
|
|
10
|
-
import type { CommitLockFn, ArtifactCommitCoordinator, FinalizeResult, FinalizeTxClient } from './commit/types.js';
|
|
10
|
+
import type { AuthorizeCommit, CommitLockFn, ArtifactCommitCoordinator, FinalizeResult, FinalizeTxClient } from './commit/types.js';
|
|
11
11
|
export { DEFAULT_PUBLICATION_LIMITS, type PublicationLimits } from './publication-limits.js';
|
|
12
12
|
export interface ReleaseRecord {
|
|
13
13
|
releaseId: string;
|
|
@@ -127,6 +127,8 @@ export interface GuardedPublishInput extends PublishInput {
|
|
|
127
127
|
readonly identity: CommandIdentity;
|
|
128
128
|
readonly actorId: string;
|
|
129
129
|
readonly policyRevision: string;
|
|
130
|
+
/** Request-bound host approval guard; defaults to participant.checkApproval. */
|
|
131
|
+
readonly authorizeCommit?: AuthorizeCommit;
|
|
130
132
|
readonly callerKey?: string;
|
|
131
133
|
readonly attemptId?: string;
|
|
132
134
|
readonly invocationId?: string;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* serve unit tests and the Postgres/S3 adapters serve integration.
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
+
import { assertBlobPrefix } from './blob-prefix.js';
|
|
15
16
|
import { canonicalManifest, manifestDigest } from '../codecs.js';
|
|
16
17
|
import { parseSourceRef } from './sources.js';
|
|
17
18
|
import { publicationLimits, validatePublicationFiles } from './publication-limits.js';
|
|
@@ -59,8 +60,9 @@ export function createMemoryBlobStore() {
|
|
|
59
60
|
getBlob(key) {
|
|
60
61
|
return Promise.resolve(blobs.get(key));
|
|
61
62
|
},
|
|
62
|
-
listKeys(prefix) {
|
|
63
|
-
|
|
63
|
+
async listKeys(prefix) {
|
|
64
|
+
assertBlobPrefix(prefix);
|
|
65
|
+
return [...blobs.keys()].filter((key) => key.startsWith(prefix)).sort();
|
|
64
66
|
},
|
|
65
67
|
deleteBlob(key) {
|
|
66
68
|
if (!blobs.has(key))
|
|
@@ -247,7 +249,7 @@ export function createGuardedPublicationService(deps) {
|
|
|
247
249
|
const cleanupWindowMs = deps.cleanupWindowMs ?? DEFAULT_CLEANUP_WINDOW_MS;
|
|
248
250
|
if (!Number.isSafeInteger(cleanupWindowMs) || cleanupWindowMs < 1)
|
|
249
251
|
throw new Error('invalid preparation lifetime');
|
|
250
|
-
const guard = (client) => async (context, transaction) => {
|
|
252
|
+
const guard = (client, authorizeCommit) => async (context, transaction) => {
|
|
251
253
|
const { state, acl } = await deps.createUnitWriter(client()).readAuthorization(context.artifactId);
|
|
252
254
|
const verdict = decidePublishAuthorization({ lifecycleState: state, acl, actorId: context.actorId });
|
|
253
255
|
if (!verdict.allowed) {
|
|
@@ -259,19 +261,6 @@ export function createGuardedPublicationService(deps) {
|
|
|
259
261
|
retriable: false,
|
|
260
262
|
};
|
|
261
263
|
}
|
|
262
|
-
const approval = await transaction.participant.checkApproval({
|
|
263
|
-
actorId: context.actorId,
|
|
264
|
-
operation: context.operation,
|
|
265
|
-
});
|
|
266
|
-
if (!approval.approved) {
|
|
267
|
-
return {
|
|
268
|
-
allowed: false,
|
|
269
|
-
reason: approval.reason ?? 'denied',
|
|
270
|
-
publicCode: 'access_denied',
|
|
271
|
-
status: 403,
|
|
272
|
-
retriable: false,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
264
|
const revision = await transaction.participant.readRevision();
|
|
276
265
|
if (revision !== context.policyRevision) {
|
|
277
266
|
return {
|
|
@@ -282,6 +271,12 @@ export function createGuardedPublicationService(deps) {
|
|
|
282
271
|
retriable: false,
|
|
283
272
|
};
|
|
284
273
|
}
|
|
274
|
+
if (authorizeCommit !== undefined)
|
|
275
|
+
return authorizeCommit(context, transaction);
|
|
276
|
+
const approval = await transaction.participant.checkApproval({ actorId: context.actorId, operation: context.operation });
|
|
277
|
+
if (!approval.approved) {
|
|
278
|
+
return { allowed: false, reason: approval.reason ?? 'denied', publicCode: 'access_denied', status: 403, retriable: false };
|
|
279
|
+
}
|
|
285
280
|
return { allowed: true, fence: transaction.participant.mintFence(revision, context.invocationId) };
|
|
286
281
|
};
|
|
287
282
|
return {
|
|
@@ -365,7 +360,7 @@ export function createGuardedPublicationService(deps) {
|
|
|
365
360
|
if (finalizationClient === undefined)
|
|
366
361
|
throw new Error('publication guard needs the finalization client');
|
|
367
362
|
return finalizationClient;
|
|
368
|
-
}),
|
|
363
|
+
}, input.authorizeCommit),
|
|
369
364
|
finishIntents: async (client) => {
|
|
370
365
|
await deps.createUnitWriter(client).completeIntent(intent.intentId);
|
|
371
366
|
},
|
package/docs/migration-l6.md
CHANGED
|
@@ -83,7 +83,12 @@ under this lock and compares it to the stored receipt. Missing or changed
|
|
|
83
83
|
targets deny before the host guard; explicit replay requires a binding.
|
|
84
84
|
|
|
85
85
|
Use `createGuardedEnsureService` and `createGuardedPublicationService` for
|
|
86
|
-
custom services.
|
|
86
|
+
custom services. Pass the request-bound host guard as `publishFinalized(input)`'s
|
|
87
|
+
optional `authorizeCommit` field to preserve host approval policy and refusal codes;
|
|
88
|
+
it runs after lifecycle, ACL and policy revision checks. When omitted, publication
|
|
89
|
+
uses `participant.checkApproval` and denies unapproved or expired commits.
|
|
90
|
+
HTTP/MCP pass the invocation's host guard automatically.
|
|
91
|
+
The canonical coordinator exposes `finalizeOperation` and
|
|
87
92
|
`replayOperation` with the frozen L0 inputs. Bind fresh domain request plans
|
|
88
93
|
through its `operations` option; no process-local replay cache is used.
|
|
89
94
|
Provider adapters use `finalizePrepared` and `replayPrepared` on that same
|
|
@@ -206,12 +206,15 @@ export function createRadarCommitSetup(options: {
|
|
|
206
206
|
pool: Pool;
|
|
207
207
|
clock: { nowMs: number };
|
|
208
208
|
quotaKey?: string;
|
|
209
|
+
/** Bind this same mapper on createHttpArtifactApp and createArtifactsAdapter. */
|
|
210
|
+
mapHostError?: CommitCoordinatorOptions['mapHostError'];
|
|
209
211
|
testObserver?: CommitCoordinatorOptions['testObserver'];
|
|
210
212
|
testPoints?: readonly CoordinatorTestPoint[];
|
|
211
213
|
}): RadarCommitSetup {
|
|
212
214
|
const quotaKey = options.quotaKey ?? 'radar-quota';
|
|
213
215
|
const abandonedAttempts: AbandonedAttempt[] = [];
|
|
214
216
|
const coordinator = createArtifactCommitCoordinator({
|
|
217
|
+
mapHostError: options.mapHostError,
|
|
215
218
|
executor: createPostgresCommitExecutor(options.pool),
|
|
216
219
|
policy: createParticipantFactory(quotaKey, options.clock),
|
|
217
220
|
nowMs: () => options.clock.nowMs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentropic/artifacts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|