@sentropic/artifacts 0.2.5 → 0.2.7

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 (48) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +20 -6
  3. package/dist/adapters/postgres.js +11 -4
  4. package/dist/http.d.ts +4 -3
  5. package/dist/http.js +225 -37
  6. package/dist/mcp-core.d.ts +3 -3
  7. package/dist/mcp-core.js +23 -10
  8. package/dist/profile/contracts.d.ts +15 -5
  9. package/dist/profile/contracts.js +9 -6
  10. package/dist/profile/index.js +33 -4
  11. package/dist/profile/radar.js +4 -1
  12. package/dist/profile/schemas.js +35 -6
  13. package/dist/profile/validation.d.ts +3 -1
  14. package/dist/profile/validation.js +32 -13
  15. package/dist/server/access.d.ts +8 -0
  16. package/dist/server/command-identity.d.ts +4 -2
  17. package/dist/server/command-identity.js +5 -5
  18. package/dist/server/commit/coordinator.js +12 -9
  19. package/dist/server/commit/types.d.ts +4 -0
  20. package/dist/server/edits.d.ts +17 -0
  21. package/dist/server/edits.js +43 -0
  22. package/dist/server/failures.d.ts +31 -2
  23. package/dist/server/failures.js +14 -3
  24. package/dist/server/lifecycle.d.ts +2 -0
  25. package/dist/server/lifecycle.js +3 -0
  26. package/dist/server/management.d.ts +1 -0
  27. package/dist/server/management.js +9 -2
  28. package/dist/server/mime.d.ts +7 -0
  29. package/dist/server/mime.js +7 -0
  30. package/dist/server/outcome-projection.d.ts +2 -2
  31. package/dist/server/outcome-projection.js +25 -9
  32. package/dist/server/publication.d.ts +18 -1
  33. package/dist/server/publication.js +54 -5
  34. package/dist/server/result-schema.d.ts +14 -0
  35. package/dist/server/result-schema.js +43 -0
  36. package/dist/server/service.d.ts +2 -0
  37. package/dist/server/service.js +2 -0
  38. package/dist/server/source-read.d.ts +23 -3
  39. package/dist/server/source-read.js +17 -2
  40. package/dist/server/summary.d.ts +4 -3
  41. package/dist/server/summary.js +4 -3
  42. package/dist/server/templates.d.ts +5 -0
  43. package/dist/server/templates.js +30 -3
  44. package/dist/server/versions.d.ts +29 -0
  45. package/dist/server/versions.js +39 -0
  46. package/docs/migration-l6.md +8 -1
  47. package/migrations/0005_metadata.sql +14 -0
  48. package/package.json +2 -2
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parseManifest } from '../codecs.js';
3
3
  import { ArtifactOperationError } from './failures.js';
4
+ import { TEXT_MIME_RE } from './mime.js';
5
+ import { API_GUIDE_REF } from './templates.js';
4
6
  /** Read author bytes only. Provenance never substitutes for a stored snapshot. */
5
7
  export async function readArtifactSource(ports, input) {
6
8
  await ports.authorize();
@@ -59,10 +61,23 @@ export async function readArtifactSource(ports, input) {
59
61
  createHash('sha256').update(bytes).digest('hex') !== file.digest.slice(7)) {
60
62
  throw new ArtifactOperationError('verification_failed');
61
63
  }
62
- files.push({ ...file, bytes: Array.from(bytes) });
64
+ // Verified raw bytes are encoded for the wire: text mime types as UTF-8
65
+ // text, everything else (including non-UTF-8 "text") as base64. Never an
66
+ // integer array, which blows the tool-result budget on small pages.
67
+ let text;
68
+ if (TEXT_MIME_RE.test(file.mimeType)) {
69
+ try {
70
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
71
+ }
72
+ catch { /* Not valid UTF-8: fall back to base64. */ }
73
+ }
74
+ files.push({ ...file, ...(text !== undefined
75
+ ? { text, encoding: 'utf8' }
76
+ : { base64: Buffer.from(bytes).toString('base64'), encoding: 'base64' }) });
63
77
  }
64
78
  const sourceRef = await ports.getSource(input.artifactId) ?? null;
65
79
  await ports.authorize();
66
80
  return { artifactId: input.artifactId, releaseId: release.releaseId, digest: manifest.digest,
67
- manifest: release.manifest, files, sourceRef, ...ports.urls?.(input.artifactId, release.releaseId) };
81
+ manifest: release.manifest, files, sourceRef, guideRef: ports.guideRef ?? API_GUIDE_REF,
82
+ ...ports.urls?.(input.artifactId, release.releaseId) };
68
83
  }
@@ -20,9 +20,7 @@ export declare function artifactSummary(input: {
20
20
  write: boolean;
21
21
  manage: boolean;
22
22
  };
23
- icon?: string | null | undefined;
24
- description?: string | undefined;
25
- title?: string | undefined;
23
+ metadata?: import("./access.js").ArtifactMetadata | undefined;
26
24
  artifactId: string;
27
25
  bindingName: string | null;
28
26
  ownerId: string;
@@ -36,4 +34,7 @@ export declare function artifactSummary(input: {
36
34
  digest: string | null;
37
35
  stableUrl: string | null;
38
36
  pinnedUrl: string | null;
37
+ title: string;
38
+ description: string;
39
+ icon: string | null;
39
40
  };
@@ -9,9 +9,10 @@ export function artifactSummary(input) {
9
9
  createdAt: acl.createdAt ?? null, updatedAt: acl.updatedAt ?? null,
10
10
  currentReleaseId: current?.releaseId ?? null, digest: current ? parseManifest(current.manifest).digest ?? null : null,
11
11
  stableUrl: urls?.stableUrl ?? null, pinnedUrl: current ? urls?.pinnedUrl ?? null : null,
12
- ...(acl.title === undefined ? {} : { title: acl.title }),
13
- ...(acl.description === undefined ? {} : { description: acl.description }),
14
- ...(acl.icon === undefined ? {} : { icon: acl.icon }),
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 }),
15
16
  permissions: { read: evaluateAccess(acl, actorId, 'read', input.approved),
16
17
  source: evaluateAccess(acl, actorId, 'source', input.approved), write: owner, manage: owner } };
17
18
  }
@@ -29,5 +29,10 @@ export interface TemplateCatalogPort {
29
29
  }
30
30
  export declare const DEFAULT_TEMPLATE_ID = "static-html";
31
31
  export declare const DEFAULT_API_GUIDE_VERSION = "v1";
32
+ /** Wire reference returned by get_source instead of embedding the guide body. */
33
+ export declare const API_GUIDE_REF: {
34
+ readonly tool: "artifacts_get_api_guide";
35
+ readonly version: "v1";
36
+ };
32
37
  /** Generic in-package catalog; hosts override with approved local assets. */
33
38
  export declare function createDefaultTemplateCatalog(): TemplateCatalogPort;
@@ -9,6 +9,8 @@
9
9
  */
10
10
  export const DEFAULT_TEMPLATE_ID = 'static-html';
11
11
  export const DEFAULT_API_GUIDE_VERSION = 'v1';
12
+ /** Wire reference returned by get_source instead of embedding the guide body. */
13
+ export const API_GUIDE_REF = { tool: 'artifacts_get_api_guide', version: DEFAULT_API_GUIDE_VERSION };
12
14
  const INDEX_HTML = [
13
15
  '<!doctype html>',
14
16
  '<html lang="en">',
@@ -44,11 +46,36 @@ const GUIDE_V1 = [
44
46
  'Publish and share static web pages through the `artifacts_*` tools:',
45
47
  '',
46
48
  '- `artifacts_get_template` / `artifacts_get_api_guide`: start here.',
47
- '- `artifacts_ensure`: resolve (or create) your private artifact view.',
48
- '- `artifacts_publish`: publish inline files as a new release.',
49
+ '- `artifacts_ensure`: resolve (or create) your private artifact view;',
50
+ ' optional `title`, `description` and `icon` (one short word) describe it.',
51
+ '- `artifacts_publish`: publish inline `files` as a new release, or apply',
52
+ ' small `edits` (`[{path, oldStr, newStr}]`) to the current release\'s',
53
+ ' text files — `oldStr` must occur exactly once per file, otherwise the',
54
+ ' call fails with `edit_mismatch` and changes nothing. Both accept',
55
+ ' optional metadata and `expectedCurrentReleaseId`: when set and not the',
56
+ ' current release, the call fails with a `conflict` error carrying the',
57
+ ' current release id and digest so you can re-read and merge.',
58
+ ' Strict profiles require this field on publish and revert; null means unpublished.',
59
+ ' Edits target that observed release, with at most 100 ordered substitutions.',
60
+ ' Reuse the exact idempotency key after a lost response, including commitState unknown.',
61
+ ' Replays precede edits and CAS; changing metadata, edits or the precondition under',
62
+ ' the same key returns conflict with variant command_conflict.',
63
+ ' MCP returns the complete artifacts-outcome/v2 JSON envelope: ok, invocationId,',
64
+ ' commitState, replayed/output on success, or typed error details on failure.',
49
65
  '- `artifacts_create_upload` + bounded HTTP append +',
50
66
  ' `artifacts_commit_upload`: stage large bytes, then commit them.',
51
- '- `artifacts_list` / `artifacts_get` / `artifacts_get_source`: discover.',
67
+ '- `artifacts_list` / `artifacts_get`: discover (metadata included).',
68
+ '- `artifacts_list_versions`: full release history of one artifact —',
69
+ ' per release: id, created at, author, state, whether it is current,',
70
+ ' and a content digest.',
71
+ '- `artifacts_get_source`: verified file contents of the current or a selected',
72
+ ' release, for editing. Text mime types come back as `text` with',
73
+ ' `encoding: "utf8"`; every other file as `base64` with `encoding: "base64"` —',
74
+ ' never an integer array. The result carries a `guideRef` (tool + version),',
75
+ ' never the guide body. Per call: at most 32 files and 256 KiB decoded.',
76
+ '- `artifacts_revert`: make an earlier release current again as a NEW',
77
+ ' release that copies its content — the artifact URL stays stable and',
78
+ ' history only grows. Purged releases are refused with `release_unavailable`.',
52
79
  '- `artifacts_fork`: private independent copy (no ACL copied).',
53
80
  '- `artifacts_set_access`: grant/revoke read or source, toggle public.',
54
81
  '- `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
+ }
@@ -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` and additive `0003_coordinator.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,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.5",
3
+ "version": "0.2.7",
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",
@@ -208,7 +208,7 @@
208
208
  },
209
209
  "peerDependencies": {
210
210
  "@aws-sdk/client-s3": "^3.980.0",
211
- "@sentropic/mcp-platform": ">=0.3.0 <0.5.0",
211
+ "@sentropic/mcp-platform": ">=0.4.1 <0.5.0",
212
212
  "pg": "^8.12.0"
213
213
  },
214
214
  "peerDependenciesMeta": {