@sentropic/artifacts 0.2.4 → 0.2.6
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/CHANGELOG.md +34 -0
- package/README.md +51 -3
- package/dist/adapters/postgres-management.d.ts +4 -0
- package/dist/adapters/postgres-management.js +36 -0
- package/dist/adapters/postgres.js +30 -8
- package/dist/http.d.ts +11 -1
- package/dist/http.js +272 -38
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp-core.d.ts +3 -3
- package/dist/mcp-core.js +29 -10
- package/dist/profile/contracts.d.ts +15 -5
- package/dist/profile/contracts.js +9 -6
- package/dist/profile/index.js +39 -4
- package/dist/profile/radar.js +4 -1
- package/dist/profile/schemas.js +41 -8
- package/dist/profile/validation.d.ts +3 -1
- package/dist/profile/validation.js +36 -13
- package/dist/server/access.d.ts +14 -0
- package/dist/server/command-identity.d.ts +6 -3
- package/dist/server/command-identity.js +8 -8
- package/dist/server/commit/coordinator.js +13 -10
- package/dist/server/commit/types.d.ts +4 -0
- package/dist/server/edits.d.ts +17 -0
- package/dist/server/edits.js +43 -0
- package/dist/server/failures.d.ts +34 -2
- package/dist/server/failures.js +17 -3
- package/dist/server/lifecycle.d.ts +4 -0
- package/dist/server/lifecycle.js +15 -0
- package/dist/server/management.d.ts +46 -0
- package/dist/server/management.js +86 -0
- package/dist/server/outcome-projection.d.ts +2 -2
- package/dist/server/outcome-projection.js +25 -7
- package/dist/server/publication.d.ts +22 -11
- package/dist/server/publication.js +56 -7
- package/dist/server/result-schema.d.ts +14 -0
- package/dist/server/result-schema.js +35 -0
- package/dist/server/service.d.ts +2 -0
- package/dist/server/service.js +2 -0
- package/dist/server/source-read.d.ts +38 -0
- package/dist/server/source-read.js +68 -0
- package/dist/server/summary.d.ts +40 -0
- package/dist/server/summary.js +18 -0
- package/dist/server/templates.js +24 -3
- package/dist/server/versions.d.ts +29 -0
- package/dist/server/versions.js +39 -0
- package/docs/migration-l6.md +8 -1
- package/migrations/0004_terminal_controls.sql +47 -0
- package/migrations/0005_metadata.sql +14 -0
- package/package.json +15 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Published domain-result envelope; error details are selected by code and variant. */
|
|
2
|
+
const string = { type: 'string' };
|
|
3
|
+
const nullable = { type: ['string', 'null'] };
|
|
4
|
+
const object = (properties, required = Object.keys(properties)) => ({
|
|
5
|
+
type: 'object', properties, required, additionalProperties: false,
|
|
6
|
+
});
|
|
7
|
+
const error = (code, details) => object({
|
|
8
|
+
code: { enum: [code] }, message: string, retriable: { type: 'boolean' }, details,
|
|
9
|
+
});
|
|
10
|
+
const pointer = object({ variant: { enum: ['pointer_conflict'] }, artifactId: string,
|
|
11
|
+
expectedCurrentReleaseId: nullable, currentReleaseId: nullable, currentDigest: nullable,
|
|
12
|
+
currentSource: object({ artifactId: string, releaseId: string, digest: nullable,
|
|
13
|
+
manifest: string, files: { type: 'array', items: { type: 'object' } }, sourceRef: { type: ['object', 'null'] },
|
|
14
|
+
stableUrl: string, pinnedUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef']),
|
|
15
|
+
}, ['variant', 'artifactId', 'expectedCurrentReleaseId', 'currentReleaseId', 'currentDigest']);
|
|
16
|
+
const common = { invocationId: string,
|
|
17
|
+
commitState: { enum: ['not_committed', 'committed', 'unknown'] } };
|
|
18
|
+
export const ARTIFACT_RESULT_SCHEMA = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
oneOf: [
|
|
21
|
+
object({ ok: { enum: [true] }, ...common, replayed: { type: 'boolean' }, output: { type: 'object' } }),
|
|
22
|
+
object({ ok: { enum: [false] }, ...common, error: { oneOf: [
|
|
23
|
+
error('conflict', { oneOf: [pointer, object({ variant: { enum: ['command_conflict', 'key_contention', 'quota_exhausted'] } })] }),
|
|
24
|
+
error('edit_mismatch', object({ path: string, editIndex: { type: 'integer', minimum: 0 },
|
|
25
|
+
reason: { enum: ['missing', 'ambiguous', 'not_text', 'file_missing'] }, occurrences: { type: 'integer' } })),
|
|
26
|
+
error('release_unavailable', object({ reason: { enum: ['purged', 'not_retained', 'unpublished'] } })),
|
|
27
|
+
error('precondition_required', object({ profileRevisionId: string, resultEnvelopeVersion: { enum: [1, 2] },
|
|
28
|
+
requiredField: { enum: ['expectedCurrentReleaseId'] }, readTools: { type: 'array', items: string } })),
|
|
29
|
+
error('confirmation_required', object({ confirmationRef: string, managementUrl: string, expiresAt: string })),
|
|
30
|
+
object({ code: { type: 'string', not: { enum: ['conflict', 'edit_mismatch', 'release_unavailable',
|
|
31
|
+
'precondition_required', 'confirmation_required'] } },
|
|
32
|
+
message: string, retriable: { type: 'boolean' }, details: object({ variant: string }) }, ['code', 'message', 'retriable']),
|
|
33
|
+
] } }),
|
|
34
|
+
],
|
|
35
|
+
};
|
package/dist/server/service.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type CompiledArtifactsProfile } from '../profile/index.js';
|
|
|
3
3
|
import { type ArtifactKeyDerivationBinding } from './command-identity.js';
|
|
4
4
|
import type { ArtifactInvocationContext, ArtifactCommitCoordinator, FinalizeTxClient, InternalIdempotencyKey } from './commit/types.js';
|
|
5
5
|
import type { ArtifactOutcome } from './failures.js';
|
|
6
|
+
import type { ArtifactMetadata } from './access.js';
|
|
6
7
|
export interface EnsurePrivateViewInput {
|
|
7
8
|
actorUserId: string;
|
|
8
9
|
ownerId: string;
|
|
@@ -48,6 +49,7 @@ export interface GuardedEnsureResult {
|
|
|
48
49
|
}
|
|
49
50
|
/** Binding identity resolved inside the finalization unit. */
|
|
50
51
|
export interface EnsureBindingInput {
|
|
52
|
+
readonly metadata?: ArtifactMetadata;
|
|
51
53
|
readonly ownerId: string;
|
|
52
54
|
readonly storageScopeId: string;
|
|
53
55
|
readonly bindingName: string;
|
package/dist/server/service.js
CHANGED
|
@@ -88,7 +88,9 @@ const ensureResult = (receipt, replayed, invocationId) => ({
|
|
|
88
88
|
});
|
|
89
89
|
export function createGuardedEnsureService(deps) {
|
|
90
90
|
const finalizeRequest = (invocation, command, policyRevision) => {
|
|
91
|
+
const metadata = Object.fromEntries(['title', 'description', 'icon'].filter(key => Object.hasOwn(command.payload, key)).map(key => [key, command.payload[key]]));
|
|
91
92
|
const binding = {
|
|
93
|
+
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
92
94
|
ownerId: invocation.actorId,
|
|
93
95
|
storageScopeId: invocation.storageScopeId,
|
|
94
96
|
bindingName: command.payload.bindingName,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SourceRef } from './sources.js';
|
|
2
|
+
import type { PublicationService } from './publication.js';
|
|
3
|
+
export interface SourceReadPorts {
|
|
4
|
+
storageScopeId?: string;
|
|
5
|
+
authorize(): Promise<void>;
|
|
6
|
+
getCurrent: PublicationService['getCurrent'];
|
|
7
|
+
getRelease: PublicationService['getRelease'];
|
|
8
|
+
getSource(artifactId: string): Promise<SourceRef | undefined>;
|
|
9
|
+
getBlob(key: string): Promise<Uint8Array | undefined>;
|
|
10
|
+
urls?: (artifactId: string, releaseId: string) => {
|
|
11
|
+
stableUrl: string;
|
|
12
|
+
pinnedUrl: string;
|
|
13
|
+
};
|
|
14
|
+
maxFiles: number;
|
|
15
|
+
maxTotalBytes: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SourceReadInput {
|
|
18
|
+
artifactId: string;
|
|
19
|
+
releaseId?: string;
|
|
20
|
+
paths?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** Read author bytes only. Provenance never substitutes for a stored snapshot. */
|
|
23
|
+
export declare function readArtifactSource(ports: SourceReadPorts, input: SourceReadInput): Promise<{
|
|
24
|
+
stableUrl?: string | undefined;
|
|
25
|
+
pinnedUrl?: string | undefined;
|
|
26
|
+
artifactId: string;
|
|
27
|
+
releaseId: string;
|
|
28
|
+
digest: string;
|
|
29
|
+
manifest: string;
|
|
30
|
+
files: {
|
|
31
|
+
bytes: number[];
|
|
32
|
+
path: string;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
digest: string;
|
|
35
|
+
size: number;
|
|
36
|
+
}[];
|
|
37
|
+
sourceRef: SourceRef | null;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseManifest } from '../codecs.js';
|
|
3
|
+
import { ArtifactOperationError } from './failures.js';
|
|
4
|
+
/** Read author bytes only. Provenance never substitutes for a stored snapshot. */
|
|
5
|
+
export async function readArtifactSource(ports, input) {
|
|
6
|
+
await ports.authorize();
|
|
7
|
+
const release = input.releaseId === undefined ? await ports.getCurrent(input.artifactId)
|
|
8
|
+
: await ports.getRelease(input.releaseId);
|
|
9
|
+
if (!release)
|
|
10
|
+
throw input.releaseId ? new ArtifactOperationError('not_found')
|
|
11
|
+
: new ArtifactOperationError('release_unavailable', { publicDetails: { reason: 'unpublished' } });
|
|
12
|
+
if ((input.releaseId !== undefined && release.releaseId !== input.releaseId) ||
|
|
13
|
+
release.artifactId !== input.artifactId)
|
|
14
|
+
throw new ArtifactOperationError('not_found');
|
|
15
|
+
let manifest;
|
|
16
|
+
try {
|
|
17
|
+
manifest = parseManifest(release.manifest);
|
|
18
|
+
}
|
|
19
|
+
catch (cause) {
|
|
20
|
+
throw new ArtifactOperationError('verification_failed', { cause });
|
|
21
|
+
}
|
|
22
|
+
if (manifest.artifactId !== input.artifactId)
|
|
23
|
+
throw new ArtifactOperationError('not_found');
|
|
24
|
+
if (ports.storageScopeId !== undefined && manifest.storageScopeId !== ports.storageScopeId)
|
|
25
|
+
throw new ArtifactOperationError('verification_failed');
|
|
26
|
+
if (release.manifestDigest !== manifest.digest)
|
|
27
|
+
throw new ArtifactOperationError('verification_failed');
|
|
28
|
+
if (release.state === 'purged')
|
|
29
|
+
throw new ArtifactOperationError('release_unavailable', { publicDetails: { reason: 'purged' } });
|
|
30
|
+
if (!manifest.digest || !Array.isArray(manifest.files) || manifest.files.length > ports.maxFiles) {
|
|
31
|
+
throw new ArtifactOperationError('verification_failed');
|
|
32
|
+
}
|
|
33
|
+
const entries = [];
|
|
34
|
+
const paths = new Set();
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const value of manifest.files) {
|
|
37
|
+
const file = value;
|
|
38
|
+
if (!file || typeof file.path !== 'string' || typeof file.mimeType !== 'string' ||
|
|
39
|
+
!/^sha256:[0-9a-f]{64}$/.test(file.digest) || !Number.isSafeInteger(file.size) || file.size < 0 ||
|
|
40
|
+
paths.has(file.path))
|
|
41
|
+
throw new ArtifactOperationError('verification_failed');
|
|
42
|
+
total += file.size;
|
|
43
|
+
if (total > ports.maxTotalBytes)
|
|
44
|
+
throw new ArtifactOperationError('limit_exceeded');
|
|
45
|
+
paths.add(file.path);
|
|
46
|
+
entries.push(file);
|
|
47
|
+
}
|
|
48
|
+
if (input.paths !== undefined && (!input.paths.length || input.paths.length > ports.maxFiles ||
|
|
49
|
+
new Set(input.paths).size !== input.paths.length || input.paths.some((path) => !paths.has(path)))) {
|
|
50
|
+
throw new ArtifactOperationError('invalid_request');
|
|
51
|
+
}
|
|
52
|
+
const selected = input.paths === undefined ? entries : entries.filter((file) => input.paths.includes(file.path));
|
|
53
|
+
const files = [];
|
|
54
|
+
for (const file of selected) {
|
|
55
|
+
const bytes = await ports.getBlob(`artifacts/${manifest.storageScopeId}/blobs/sha256/${file.digest.slice(7)}`);
|
|
56
|
+
if (!bytes)
|
|
57
|
+
throw new ArtifactOperationError('release_unavailable', { publicDetails: { reason: 'not_retained' } });
|
|
58
|
+
if (bytes.length !== file.size ||
|
|
59
|
+
createHash('sha256').update(bytes).digest('hex') !== file.digest.slice(7)) {
|
|
60
|
+
throw new ArtifactOperationError('verification_failed');
|
|
61
|
+
}
|
|
62
|
+
files.push({ ...file, bytes: Array.from(bytes) });
|
|
63
|
+
}
|
|
64
|
+
const sourceRef = await ports.getSource(input.artifactId) ?? null;
|
|
65
|
+
await ports.authorize();
|
|
66
|
+
return { artifactId: input.artifactId, releaseId: release.releaseId, digest: manifest.digest,
|
|
67
|
+
manifest: release.manifest, files, sourceRef, ...ports.urls?.(input.artifactId, release.releaseId) };
|
|
68
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ArtifactAcl } from './access.js';
|
|
2
|
+
export declare function artifactSummary(input: {
|
|
3
|
+
artifactId: string;
|
|
4
|
+
acl: ArtifactAcl;
|
|
5
|
+
actorId: string;
|
|
6
|
+
state: string;
|
|
7
|
+
current?: {
|
|
8
|
+
releaseId: string;
|
|
9
|
+
manifest: string;
|
|
10
|
+
};
|
|
11
|
+
urls?: (artifactId: string, releaseId: string) => {
|
|
12
|
+
stableUrl: string;
|
|
13
|
+
pinnedUrl: string;
|
|
14
|
+
};
|
|
15
|
+
approved?: (principalId: string) => boolean;
|
|
16
|
+
}): {
|
|
17
|
+
permissions: {
|
|
18
|
+
read: boolean;
|
|
19
|
+
source: boolean;
|
|
20
|
+
write: boolean;
|
|
21
|
+
manage: boolean;
|
|
22
|
+
};
|
|
23
|
+
metadata?: import("./access.js").ArtifactMetadata | undefined;
|
|
24
|
+
artifactId: string;
|
|
25
|
+
bindingName: string | null;
|
|
26
|
+
ownerId: string;
|
|
27
|
+
storageScopeId: string | undefined;
|
|
28
|
+
state: string;
|
|
29
|
+
isPublic: boolean;
|
|
30
|
+
withdrawn: boolean;
|
|
31
|
+
createdAt: string | null;
|
|
32
|
+
updatedAt: string | null;
|
|
33
|
+
currentReleaseId: string | null;
|
|
34
|
+
digest: string | null;
|
|
35
|
+
stableUrl: string | null;
|
|
36
|
+
pinnedUrl: string | null;
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
icon: string | null;
|
|
40
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseManifest } from '../codecs.js';
|
|
2
|
+
import { evaluateAccess } from './access.js';
|
|
3
|
+
export function artifactSummary(input) {
|
|
4
|
+
const { artifactId, acl, actorId, state, current } = input;
|
|
5
|
+
const urls = input.urls?.(artifactId, current?.releaseId ?? '');
|
|
6
|
+
const owner = acl.ownerId === actorId;
|
|
7
|
+
return { artifactId, bindingName: acl.bindingName ?? null, ownerId: acl.ownerId,
|
|
8
|
+
storageScopeId: acl.storageScopeId, state, isPublic: acl.isPublic, withdrawn: state === 'withdrawn',
|
|
9
|
+
createdAt: acl.createdAt ?? null, updatedAt: acl.updatedAt ?? null,
|
|
10
|
+
currentReleaseId: current?.releaseId ?? null, digest: current ? parseManifest(current.manifest).digest ?? null : null,
|
|
11
|
+
stableUrl: urls?.stableUrl ?? null, pinnedUrl: current ? urls?.pinnedUrl ?? null : null,
|
|
12
|
+
title: acl.metadata?.title ?? acl.title ?? acl.bindingName ?? artifactId,
|
|
13
|
+
description: acl.metadata?.description ?? acl.description ?? '',
|
|
14
|
+
icon: acl.metadata?.icon !== undefined ? acl.metadata.icon : acl.icon ?? null,
|
|
15
|
+
...(acl.metadata === undefined ? {} : { metadata: acl.metadata }),
|
|
16
|
+
permissions: { read: evaluateAccess(acl, actorId, 'read', input.approved),
|
|
17
|
+
source: evaluateAccess(acl, actorId, 'source', input.approved), write: owner, manage: owner } };
|
|
18
|
+
}
|
package/dist/server/templates.js
CHANGED
|
@@ -44,11 +44,32 @@ const GUIDE_V1 = [
|
|
|
44
44
|
'Publish and share static web pages through the `artifacts_*` tools:',
|
|
45
45
|
'',
|
|
46
46
|
'- `artifacts_get_template` / `artifacts_get_api_guide`: start here.',
|
|
47
|
-
'- `artifacts_ensure`: resolve (or create) your private artifact view
|
|
48
|
-
'
|
|
47
|
+
'- `artifacts_ensure`: resolve (or create) your private artifact view;',
|
|
48
|
+
' optional `title`, `description` and `icon` (one short word) describe it.',
|
|
49
|
+
'- `artifacts_publish`: publish inline `files` as a new release, or apply',
|
|
50
|
+
' small `edits` (`[{path, oldStr, newStr}]`) to the current release\'s',
|
|
51
|
+
' text files — `oldStr` must occur exactly once per file, otherwise the',
|
|
52
|
+
' call fails with `edit_mismatch` and changes nothing. Both accept',
|
|
53
|
+
' optional metadata and `expectedCurrentReleaseId`: when set and not the',
|
|
54
|
+
' current release, the call fails with a `conflict` error carrying the',
|
|
55
|
+
' current release id and digest so you can re-read and merge.',
|
|
56
|
+
' Strict profiles require this field on publish and revert; null means unpublished.',
|
|
57
|
+
' Edits target that observed release, with at most 100 ordered substitutions.',
|
|
58
|
+
' Reuse the exact idempotency key after a lost response, including commitState unknown.',
|
|
59
|
+
' Replays precede edits and CAS; changing metadata, edits or the precondition under',
|
|
60
|
+
' the same key returns conflict with variant command_conflict.',
|
|
61
|
+
' MCP returns the complete artifacts-outcome/v2 JSON envelope: ok, invocationId,',
|
|
62
|
+
' commitState, replayed/output on success, or typed error details on failure.',
|
|
49
63
|
'- `artifacts_create_upload` + bounded HTTP append +',
|
|
50
64
|
' `artifacts_commit_upload`: stage large bytes, then commit them.',
|
|
51
|
-
'- `artifacts_list` / `artifacts_get
|
|
65
|
+
'- `artifacts_list` / `artifacts_get`: discover (metadata included).',
|
|
66
|
+
'- `artifacts_list_versions`: full release history of one artifact —',
|
|
67
|
+
' per release: id, created at, author, state, whether it is current,',
|
|
68
|
+
' and a content digest.',
|
|
69
|
+
'- `artifacts_get_source`: current file contents (text decoded) for editing.',
|
|
70
|
+
'- `artifacts_revert`: make an earlier release current again as a NEW',
|
|
71
|
+
' release that copies its content — the artifact URL stays stable and',
|
|
72
|
+
' history only grows. Purged releases are refused with `release_unavailable`.',
|
|
52
73
|
'- `artifacts_fork`: private independent copy (no ACL copied).',
|
|
53
74
|
'- `artifacts_set_access`: grant/revoke read or source, toggle public.',
|
|
54
75
|
'- `artifacts_promote`: explicit version selection (expected-current).',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReleaseRecord } from './publication.js';
|
|
2
|
+
/** A cursor fixes the release boundary and is bound to the authorized artifact and scope. */
|
|
3
|
+
export declare function artifactVersions(input: {
|
|
4
|
+
artifactId: string;
|
|
5
|
+
storageScopeId: string;
|
|
6
|
+
currentReleaseId: string | null;
|
|
7
|
+
releases: ReleaseRecord[];
|
|
8
|
+
limit?: number;
|
|
9
|
+
cursor?: string;
|
|
10
|
+
state(releaseId: string): Promise<string>;
|
|
11
|
+
urls?: (artifactId: string, releaseId: string) => {
|
|
12
|
+
pinnedUrl: string;
|
|
13
|
+
};
|
|
14
|
+
}): Promise<{
|
|
15
|
+
artifactId: string;
|
|
16
|
+
currentReleaseId: string | null;
|
|
17
|
+
versions: {
|
|
18
|
+
releaseId: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
state: string;
|
|
21
|
+
authorPrincipalId: string | null;
|
|
22
|
+
parentReleaseId: string | null;
|
|
23
|
+
revertedFromReleaseId: string | null;
|
|
24
|
+
digest: string;
|
|
25
|
+
isCurrent: boolean;
|
|
26
|
+
pinnedUrl: string | null;
|
|
27
|
+
}[];
|
|
28
|
+
nextCursor: string | null;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ArtifactOperationError } from './failures.js';
|
|
2
|
+
const order = (a, b) => a.createdAt.localeCompare(b.createdAt) || a.releaseId.localeCompare(b.releaseId);
|
|
3
|
+
/** A cursor fixes the release boundary and is bound to the authorized artifact and scope. */
|
|
4
|
+
export async function artifactVersions(input) {
|
|
5
|
+
const releases = [...input.releases].sort((a, b) => order(b, a));
|
|
6
|
+
let snapshot = releases[0];
|
|
7
|
+
let after;
|
|
8
|
+
if (input.cursor !== undefined) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(Buffer.from(input.cursor, 'base64url').toString('utf8'));
|
|
11
|
+
const position = (value) => value && typeof value.createdAt === 'string' &&
|
|
12
|
+
typeof value.releaseId === 'string' && releases.some(release => order(release, value) === 0);
|
|
13
|
+
if (parsed.artifactId !== input.artifactId || parsed.storageScopeId !== input.storageScopeId ||
|
|
14
|
+
!position(parsed.snapshot) || !position(parsed.after) || order(parsed.after, parsed.snapshot) > 0)
|
|
15
|
+
throw new Error();
|
|
16
|
+
snapshot = parsed.snapshot;
|
|
17
|
+
after = parsed.after;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new ArtifactOperationError('invalid_request');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const available = releases.filter(release => snapshot && order(release, snapshot) <= 0 &&
|
|
24
|
+
(!after || order(release, after) < 0));
|
|
25
|
+
const page = available.slice(0, input.limit ?? 20);
|
|
26
|
+
const versions = await Promise.all(page.map(async (release) => {
|
|
27
|
+
const state = await input.state(release.releaseId);
|
|
28
|
+
return { releaseId: release.releaseId, createdAt: release.createdAt, state,
|
|
29
|
+
authorPrincipalId: release.authorPrincipalId ?? null, parentReleaseId: release.parentReleaseId ?? null,
|
|
30
|
+
revertedFromReleaseId: release.revertedFromReleaseId ?? null, digest: release.manifestDigest,
|
|
31
|
+
isCurrent: release.releaseId === input.currentReleaseId,
|
|
32
|
+
pinnedUrl: state === 'purged' ? null : input.urls?.(input.artifactId, release.releaseId).pinnedUrl ?? null };
|
|
33
|
+
}));
|
|
34
|
+
const last = page.at(-1);
|
|
35
|
+
const position = (value) => ({ createdAt: value.createdAt, releaseId: value.releaseId });
|
|
36
|
+
return { artifactId: input.artifactId, currentReleaseId: input.currentReleaseId, versions,
|
|
37
|
+
nextCursor: last && available.length > page.length ? Buffer.from(JSON.stringify({ artifactId: input.artifactId,
|
|
38
|
+
storageScopeId: input.storageScopeId, snapshot: position(snapshot), after: position(last) })).toString('base64url') : null };
|
|
39
|
+
}
|
package/docs/migration-l6.md
CHANGED
|
@@ -108,7 +108,14 @@ claim completion. Do not pass a raw pool to transaction-scoped deletion claims.
|
|
|
108
108
|
## Durable state and failures
|
|
109
109
|
|
|
110
110
|
Stop old writers before enabling the new composition. Apply migrations
|
|
111
|
-
`0001_artifacts.sql`, `0002_lifecycle.sql
|
|
111
|
+
`0001_artifacts.sql`, `0002_lifecycle.sql`, additive `0003_coordinator.sql`,
|
|
112
|
+
`0004_terminal_controls.sql`, and `0005_metadata.sql` (metadata and release lineage).
|
|
113
|
+
Preserve the existing host journal prefix, including activation assets, on upgrades.
|
|
114
|
+
|
|
115
|
+
R2 bindings explicitly select `profileRevisionId`, `requireExpectedCurrentReleaseId`
|
|
116
|
+
and `resultEnvelopeVersion`. The compatible p0b profile keeps full-bundle publication
|
|
117
|
+
and root results. The CAS switchover can keep result version 1; activate part 1 only
|
|
118
|
+
after qualification of result version 2. IMMO owns both switchovers and activation.
|
|
112
119
|
Preserve historical rows and objects. Supply `legacyRecords`, using
|
|
113
120
|
`createPostgresLegacyArtifactRecords(pool)` for PostgreSQL. An empty inventory
|
|
114
121
|
reader is appropriate only for a verified fresh store.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Terminal deletion retains the identity and receipts; physical purge remains host policy.
|
|
2
|
+
ALTER TABLE artifacts.artifacts ADD COLUMN IF NOT EXISTS terminal_deleted_at TIMESTAMPTZ;
|
|
3
|
+
ALTER TABLE artifacts.artifacts ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;
|
|
4
|
+
-- Defaults affect new identities only; historical modification times remain unknown.
|
|
5
|
+
ALTER TABLE artifacts.artifacts ALTER COLUMN updated_at SET DEFAULT now();
|
|
6
|
+
|
|
7
|
+
CREATE OR REPLACE FUNCTION artifacts.reject_terminal_restore() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
8
|
+
BEGIN
|
|
9
|
+
IF TG_OP = 'DELETE' THEN
|
|
10
|
+
IF OLD.terminal_deleted_at IS NOT NULL THEN RAISE EXCEPTION 'terminal artifact' USING ERRCODE = '23514'; END IF;
|
|
11
|
+
RETURN OLD;
|
|
12
|
+
END IF;
|
|
13
|
+
IF OLD.terminal_deleted_at IS NOT NULL AND
|
|
14
|
+
(NEW.terminal_deleted_at IS DISTINCT FROM OLD.terminal_deleted_at OR NEW.lifecycle_state NOT IN ('deleted', 'purged')
|
|
15
|
+
OR NEW.renewed_at IS DISTINCT FROM OLD.renewed_at) THEN
|
|
16
|
+
RAISE EXCEPTION 'terminal artifact' USING ERRCODE = '23514';
|
|
17
|
+
END IF;
|
|
18
|
+
RETURN NEW;
|
|
19
|
+
END $$;
|
|
20
|
+
DROP TRIGGER IF EXISTS terminal_restore_guard ON artifacts.artifacts;
|
|
21
|
+
CREATE TRIGGER terminal_restore_guard BEFORE UPDATE ON artifacts.artifacts
|
|
22
|
+
FOR EACH ROW EXECUTE FUNCTION artifacts.reject_terminal_restore();
|
|
23
|
+
DROP TRIGGER IF EXISTS terminal_identity_guard ON artifacts.artifacts;
|
|
24
|
+
CREATE TRIGGER terminal_identity_guard BEFORE DELETE ON artifacts.artifacts
|
|
25
|
+
FOR EACH ROW EXECUTE FUNCTION artifacts.reject_terminal_restore();
|
|
26
|
+
|
|
27
|
+
CREATE OR REPLACE FUNCTION artifacts.reject_terminal_publication() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
28
|
+
BEGIN
|
|
29
|
+
PERFORM id FROM artifacts.artifacts WHERE id = NEW.artifact_id AND terminal_deleted_at IS NULL FOR KEY SHARE;
|
|
30
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'terminal artifact' USING ERRCODE = '23514'; END IF;
|
|
31
|
+
RETURN NEW;
|
|
32
|
+
END $$;
|
|
33
|
+
DROP TRIGGER IF EXISTS terminal_release_guard ON artifacts.releases;
|
|
34
|
+
CREATE TRIGGER terminal_release_guard BEFORE INSERT ON artifacts.releases
|
|
35
|
+
FOR EACH ROW EXECUTE FUNCTION artifacts.reject_terminal_publication();
|
|
36
|
+
|
|
37
|
+
CREATE OR REPLACE FUNCTION artifacts.record_publication_update() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
38
|
+
BEGIN
|
|
39
|
+
UPDATE artifacts.artifacts SET updated_at = NEW.created_at WHERE id = NEW.artifact_id;
|
|
40
|
+
RETURN NEW;
|
|
41
|
+
END $$;
|
|
42
|
+
DROP TRIGGER IF EXISTS publication_updated_at ON artifacts.releases;
|
|
43
|
+
CREATE TRIGGER publication_updated_at AFTER INSERT ON artifacts.releases
|
|
44
|
+
FOR EACH ROW EXECUTE FUNCTION artifacts.record_publication_update();
|
|
45
|
+
DROP TRIGGER IF EXISTS terminal_pointer_guard ON artifacts.artifact_current;
|
|
46
|
+
CREATE TRIGGER terminal_pointer_guard BEFORE INSERT OR UPDATE ON artifacts.artifact_current
|
|
47
|
+
FOR EACH ROW EXECUTE FUNCTION artifacts.reject_terminal_publication();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Parity P0 — artifact display metadata (title/description/icon).
|
|
2
|
+
--
|
|
3
|
+
-- Additive upgrade after 0004_terminal_controls: no column is dropped or narrowed, existing
|
|
4
|
+
-- rows keep working (default '{}' preserves "no metadata" semantics). The host
|
|
5
|
+
-- applies this file through its existing migration journal.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE artifacts.artifacts
|
|
8
|
+
ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
9
|
+
|
|
10
|
+
-- Historical authors and lineage remain unknown; populate these on new commits only.
|
|
11
|
+
ALTER TABLE artifacts.releases
|
|
12
|
+
ADD COLUMN IF NOT EXISTS author_principal_id TEXT,
|
|
13
|
+
ADD COLUMN IF NOT EXISTS parent_release_id UUID,
|
|
14
|
+
ADD COLUMN IF NOT EXISTS reverted_from_release_id UUID;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentropic/artifacts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|
|
@@ -103,6 +103,18 @@
|
|
|
103
103
|
"types": "./dist/server/sources.d.ts",
|
|
104
104
|
"import": "./dist/server/sources.js"
|
|
105
105
|
},
|
|
106
|
+
"./server/source-read": {
|
|
107
|
+
"types": "./dist/server/source-read.d.ts",
|
|
108
|
+
"import": "./dist/server/source-read.js"
|
|
109
|
+
},
|
|
110
|
+
"./server/management": {
|
|
111
|
+
"types": "./dist/server/management.d.ts",
|
|
112
|
+
"import": "./dist/server/management.js"
|
|
113
|
+
},
|
|
114
|
+
"./adapters/postgres-management": {
|
|
115
|
+
"types": "./dist/adapters/postgres-management.d.ts",
|
|
116
|
+
"import": "./dist/adapters/postgres-management.js"
|
|
117
|
+
},
|
|
106
118
|
"./server/fork": {
|
|
107
119
|
"types": "./dist/server/fork.d.ts",
|
|
108
120
|
"import": "./dist/server/fork.js"
|
|
@@ -181,6 +193,7 @@
|
|
|
181
193
|
"docs/migration-l6.md",
|
|
182
194
|
"examples/radar-commit-setup.ts",
|
|
183
195
|
"README.md",
|
|
196
|
+
"CHANGELOG.md",
|
|
184
197
|
"LICENSE"
|
|
185
198
|
],
|
|
186
199
|
"publishConfig": {
|
|
@@ -195,7 +208,7 @@
|
|
|
195
208
|
},
|
|
196
209
|
"peerDependencies": {
|
|
197
210
|
"@aws-sdk/client-s3": "^3.980.0",
|
|
198
|
-
"@sentropic/mcp-platform": ">=0.
|
|
211
|
+
"@sentropic/mcp-platform": ">=0.4.1 <0.5.0",
|
|
199
212
|
"pg": "^8.12.0"
|
|
200
213
|
},
|
|
201
214
|
"peerDependenciesMeta": {
|