@sentropic/artifacts 0.2.7 → 0.2.8

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.8
4
+
5
+ - Content hosts use a single UUID label: `https://<id>.<base>/` resolves server-side,
6
+ either an artifact id (serves its current release) or a release id (serves that
7
+ frozen version). `parseContentHost` now returns `{ kind: 'id', id }` for exactly one
8
+ UUID label and nothing else; the former `<id>.pinned.<base>` form, two-label hosts and
9
+ foreign bases do not route.
10
+ - Unknown ids return a uniform 404: the artifact and release lookups always both run,
11
+ and "no such artifact" and "no such release" share the same response body and timing
12
+ class, without revealing which kind exists. Digest verification and the deny rules
13
+ (service workers, traversal) are unchanged.
14
+ - One wildcard per base at the edge: the `*.pinned.<base>` TLS host and ingress rule are
15
+ removed from the prod and preprod artifact-content overlays. No legacy fallback; immo
16
+ handles 301 redirects for its old hosts on its side.
17
+ - Rename decision (recorded): the `pinnedUrl` field is renamed to `versionUrl` in the
18
+ `publicationUrls` hook, the summary, versions and source-read outputs and the result
19
+ schema, now holding `https://<releaseId>.<base>/`. The converged contract allows the
20
+ rename: no consumer outside `packages/artifacts` references `pinnedUrl`/`stableUrl`
21
+ (verified by grep over ui/contracts/e2e/api/cluster-mesh), and every in-package
22
+ consumer is updated atomically in this release.
23
+
3
24
  ## 0.2.7
4
25
 
5
26
  - Fix the `artifacts_get_source` wire payload that broke page editing in claude.ai:
package/README.md CHANGED
@@ -386,8 +386,10 @@ a registrable domain. Assets within the same artifact origin remain loadable.
386
386
  - `src/content/grants.ts` — 55s HMAC content grants (60s cutoff budget) and
387
387
  single-use activation tickets bound to subject, artifact, exact release
388
388
  and channel (`stable` or `pinned`); secrets and clocks are injected.
389
- - `src/content/routing.ts` — stable (`<artifactId>.<base>`) and pinned
390
- (`<releaseId>.pinned.<base>`) single-UUID origins; safe path resolution.
389
+ - `src/content/routing.ts` — one wildcard per base with a single UUID label
390
+ `<id>.<base>`, resolved server-side as an artifact (current release) or a
391
+ release (frozen version); the former `<id>.pinned.<base>` form no longer
392
+ routes; safe path resolution.
391
393
  - `src/content/headers.ts` — no-store/nosniff/frame-denial/CSP headers and
392
394
  the host-only `__Host-artifacts_grant` cookie.
393
395
  - `src/content/handler.ts` — transport-free content serving: per-version
@@ -403,12 +405,14 @@ a registrable domain. Assets within the same artifact origin remain loadable.
403
405
  definition wrapper (the optional peer is required here and by the root barrel).
404
406
 
405
407
  Ticket issuance and redemption both require `subjectId`, `artifactId`,
406
- `releaseId` and `channel`. The handler checks the release resolved from the
407
- requested origin and its channel before minting a grant. A stable ticket
408
- issued for r1 cannot activate r2 after republish, and a stable ticket cannot
409
- activate a pinned origin (or vice versa). Mismatches return 401 without a
410
- grant cookie and preserve the ticket for a matching request. Tickets expire
411
- at `expiresAt` (120 seconds by default) and can be redeemed only once.
408
+ `releaseId` and `channel`. The handler resolves the single UUID label
409
+ server-side — an artifact id serves its current release, a release id serves
410
+ that frozen version — and checks the resolved release and its channel before
411
+ minting a grant. A stable ticket issued for r1 cannot activate r2 after
412
+ republish, and a stable ticket cannot activate a release id (or vice versa).
413
+ Mismatches return 401 without a grant cookie and preserve the ticket for a
414
+ matching request. Tickets expire at `expiresAt` (120 seconds by default) and
415
+ can be redeemed only once.
412
416
 
413
417
  ## First publish (bootstrap)
414
418
 
@@ -1,13 +1,17 @@
1
1
  /**
2
- * L-5d — transport-free content handler (spec-v8 §9 5d). Serves raw
3
- * release bytes on the separate content domain (never HTML on the
4
- * control origin). Per-version activation: a single-use ticket redeemed
5
- * by the host-authenticated subject mints a short subject+artifact+
6
- * release grant as a host-only cookie. Every request re-verifies the
7
- * grant AND live ACL/lifecycle state, so withdrawal cuts readers at the
8
- * next request (inside the 60s budget, no cache reliance). Republish
9
- * demands fresh consent on the stable origin (401); pinned origins keep
10
- * their release. Unknown/unreadable → same 404; bad grants → 401.
2
+ * L-5d — transport-free content handler (owner decision relayed by immo).
3
+ * Serves raw release bytes on the separate content domain (never HTML on
4
+ * the control origin). One wildcard per base: the single UUID label
5
+ * `<id>.<base>` is resolved server-side — an artifact id serves its
6
+ * current release, a release id serves that frozen version; an unknown id
7
+ * gets the same 404 response and timing class whatever kind it would be.
8
+ * Per-version activation: a single-use ticket redeemed by the
9
+ * host-authenticated subject mints a short subject+artifact+release grant
10
+ * as a host-only cookie. Every request re-verifies the grant AND live
11
+ * ACL/lifecycle state, so withdrawal cuts readers at the next request
12
+ * (inside the 60s budget, no cache reliance). Republish demands fresh
13
+ * consent on an artifact id (401); a release id keeps its release.
14
+ * Unknown/unreadable → same 404; bad grants → 401.
11
15
  */
12
16
  import { errorBody } from '../http.js';
13
17
  import { CONTENT_GRANT_TTL_SECONDS } from './grants.js';
@@ -65,29 +69,36 @@ export function createContentHandler(deps) {
65
69
  const origin = parseContentHost(request.host, deps.baseDomain);
66
70
  if (origin === undefined)
67
71
  return fail(404);
72
+ // One label, resolved server-side: an artifact id serves its current
73
+ // release, a release id serves that frozen version. Both lookups
74
+ // always run, so an unknown id gets the same 404 and timing class
75
+ // whether it is a missing artifact or a missing release.
76
+ const [current, byRelease] = await Promise.all([
77
+ deps.publication.getCurrent(origin.id),
78
+ deps.publication.getRelease(origin.id),
79
+ ]);
68
80
  let artifactId;
69
81
  let release;
70
- if (origin.kind === 'stable') {
71
- artifactId = origin.artifactId;
72
- release = await deps.publication.getCurrent(artifactId);
82
+ let channel;
83
+ if (current !== undefined) {
84
+ artifactId = origin.id;
85
+ release = current;
86
+ channel = 'stable';
73
87
  }
74
- else {
75
- release = await deps.publication.getRelease(origin.releaseId);
76
- if (release === undefined)
77
- return fail(404);
78
- const owner = JSON.parse(release.manifest).artifactId;
79
- if (typeof owner !== 'string')
80
- return fail(404);
81
- artifactId = owner;
88
+ else if (byRelease !== undefined) {
89
+ artifactId = byRelease.artifactId;
90
+ release = byRelease;
91
+ channel = 'pinned';
82
92
  }
83
- if (release === undefined)
93
+ else {
84
94
  return fail(404);
95
+ }
85
96
  if (request.ticket !== undefined) {
86
97
  if (request.actorId === undefined || request.actorId.length === 0)
87
98
  return fail(401);
88
99
  try {
89
100
  await deps.tickets.redeem(request.ticket, {
90
- subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
101
+ subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel,
91
102
  });
92
103
  }
93
104
  catch {
@@ -1,9 +1,6 @@
1
1
  export type ContentOrigin = {
2
- kind: 'stable';
3
- artifactId: string;
4
- } | {
5
- kind: 'pinned';
6
- releaseId: string;
2
+ kind: 'id';
3
+ id: string;
7
4
  };
8
5
  /** Parse a content Host header against the configured base domain. */
9
6
  export declare function parseContentHost(host: string, baseDomain: string): ContentOrigin | undefined;
@@ -1,11 +1,10 @@
1
1
  /**
2
- * L-5d — content routing (spec-v8 §9 5d).
3
- *
4
- * Two origins per artifact, each with a single UUID label: the stable
5
- * origin `<artifactId>.<base>` follows the current release; the pinned
6
- * origin `<releaseId>.pinned.<base>` serves one immutable release. Two
7
- * concatenated UUIDs, extra labels, foreign bases and non-UUID labels
8
- * never route. Paths normalize to manifest file paths with an
2
+ * L-5d — content routing (owner decision relayed by immo): one wildcard
3
+ * per base with a single UUID label `<id>.<base>`. The label is resolved
4
+ * server-side by the handler — an artifact id serves its current release,
5
+ * a release id serves that frozen version. The former `<id>.pinned.<base>`
6
+ * form, two labels, foreign bases and non-UUID labels never route (no
7
+ * legacy fallback). Paths normalize to manifest file paths with an
9
8
  * `index.html` default; traversal, empty segments, backslashes and
10
9
  * encoded traversal never resolve (no history fallback hiding missing
11
10
  * assets). Service workers are detectable so the handler can deny them.
@@ -21,10 +20,7 @@ export function parseContentHost(host, baseDomain) {
21
20
  return undefined;
22
21
  const labels = normalized.slice(0, normalized.length - base.length - 1).split('.');
23
22
  if (labels.length === 1 && isArtifactId(labels[0])) {
24
- return { kind: 'stable', artifactId: labels[0] };
25
- }
26
- if (labels.length === 2 && labels[1] === 'pinned' && isArtifactId(labels[0])) {
27
- return { kind: 'pinned', releaseId: labels[0] };
23
+ return { kind: 'id', id: labels[0] };
28
24
  }
29
25
  return undefined;
30
26
  }
package/dist/http.d.ts CHANGED
@@ -51,10 +51,10 @@ export interface HttpAppDeps {
51
51
  acls: AclStore;
52
52
  lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead' | 'getReleaseState'>;
53
53
  isApprovedAccount?: (principalId: string) => boolean;
54
- /** One trusted URL policy shared by HTTP and MCP receipts. */
54
+ /** One trusted URL policy shared by HTTP and MCP receipts: the artifact page and the exact version page. */
55
55
  publicationUrls?: (artifactId: string, releaseId: string) => {
56
56
  stableUrl: string;
57
- pinnedUrl: string;
57
+ versionUrl: string;
58
58
  };
59
59
  }
60
60
  export interface ArtifactApp {
@@ -10,7 +10,7 @@ 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.7";
13
+ export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.8";
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", "artifacts_list_versions", "artifacts_revert"];
15
15
  export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
16
16
  export declare const ARTIFACTS_TOOL_SPECS: {
package/dist/mcp-core.js CHANGED
@@ -7,7 +7,7 @@ export * from './profile/index.js';
7
7
  export { createRadarKeyDerivation } from './server/command-identity.js';
8
8
  export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
9
9
  export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
10
- export const ARTIFACTS_PROVIDER_VERSION = '0.2.7';
10
+ export const ARTIFACTS_PROVIDER_VERSION = '0.2.8';
11
11
  export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
12
12
  export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
13
13
  description: name === 'artifacts_get_source'
@@ -19,7 +19,7 @@ const pointer = object({ variant: { enum: ['pointer_conflict'] }, artifactId: st
19
19
  expectedCurrentReleaseId: nullable, currentReleaseId: nullable, currentDigest: nullable,
20
20
  currentSource: object({ artifactId: string, releaseId: string, digest: nullable,
21
21
  manifest: string, files: { type: 'array', items: sourceFile }, sourceRef: { type: ['object', 'null'] },
22
- guideRef, stableUrl: string, pinnedUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef', 'guideRef']),
22
+ guideRef, stableUrl: string, versionUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef', 'guideRef']),
23
23
  }, ['variant', 'artifactId', 'expectedCurrentReleaseId', 'currentReleaseId', 'currentDigest']);
24
24
  const common = { invocationId: string,
25
25
  commitState: { enum: ['not_committed', 'committed', 'unknown'] } };
@@ -9,7 +9,7 @@ export interface SourceReadPorts {
9
9
  getBlob(key: string): Promise<Uint8Array | undefined>;
10
10
  urls?: (artifactId: string, releaseId: string) => {
11
11
  stableUrl: string;
12
- pinnedUrl: string;
12
+ versionUrl: string;
13
13
  };
14
14
  /** Guide reference returned instead of embedding the guide body. */
15
15
  guideRef?: {
@@ -27,7 +27,7 @@ export interface SourceReadInput {
27
27
  /** Read author bytes only. Provenance never substitutes for a stored snapshot. */
28
28
  export declare function readArtifactSource(ports: SourceReadPorts, input: SourceReadInput): Promise<{
29
29
  stableUrl?: string | undefined;
30
- pinnedUrl?: string | undefined;
30
+ versionUrl?: string | undefined;
31
31
  artifactId: string;
32
32
  releaseId: string;
33
33
  digest: string;
@@ -10,7 +10,7 @@ export declare function artifactSummary(input: {
10
10
  };
11
11
  urls?: (artifactId: string, releaseId: string) => {
12
12
  stableUrl: string;
13
- pinnedUrl: string;
13
+ versionUrl: string;
14
14
  };
15
15
  approved?: (principalId: string) => boolean;
16
16
  }): {
@@ -33,7 +33,7 @@ export declare function artifactSummary(input: {
33
33
  currentReleaseId: string | null;
34
34
  digest: string | null;
35
35
  stableUrl: string | null;
36
- pinnedUrl: string | null;
36
+ versionUrl: string | null;
37
37
  title: string;
38
38
  description: string;
39
39
  icon: string | null;
@@ -8,7 +8,7 @@ export function artifactSummary(input) {
8
8
  storageScopeId: acl.storageScopeId, state, isPublic: acl.isPublic, withdrawn: state === 'withdrawn',
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
- stableUrl: urls?.stableUrl ?? null, pinnedUrl: current ? urls?.pinnedUrl ?? null : null,
11
+ stableUrl: urls?.stableUrl ?? null, versionUrl: current ? urls?.versionUrl ?? null : null,
12
12
  title: acl.metadata?.title ?? acl.title ?? acl.bindingName ?? artifactId,
13
13
  description: acl.metadata?.description ?? acl.description ?? '',
14
14
  icon: acl.metadata?.icon !== undefined ? acl.metadata.icon : acl.icon ?? null,
@@ -9,7 +9,7 @@ export declare function artifactVersions(input: {
9
9
  cursor?: string;
10
10
  state(releaseId: string): Promise<string>;
11
11
  urls?: (artifactId: string, releaseId: string) => {
12
- pinnedUrl: string;
12
+ versionUrl: string;
13
13
  };
14
14
  }): Promise<{
15
15
  artifactId: string;
@@ -23,7 +23,7 @@ export declare function artifactVersions(input: {
23
23
  revertedFromReleaseId: string | null;
24
24
  digest: string;
25
25
  isCurrent: boolean;
26
- pinnedUrl: string | null;
26
+ versionUrl: string | null;
27
27
  }[];
28
28
  nextCursor: string | null;
29
29
  }>;
@@ -29,7 +29,7 @@ export async function artifactVersions(input) {
29
29
  authorPrincipalId: release.authorPrincipalId ?? null, parentReleaseId: release.parentReleaseId ?? null,
30
30
  revertedFromReleaseId: release.revertedFromReleaseId ?? null, digest: release.manifestDigest,
31
31
  isCurrent: release.releaseId === input.currentReleaseId,
32
- pinnedUrl: state === 'purged' ? null : input.urls?.(input.artifactId, release.releaseId).pinnedUrl ?? null };
32
+ versionUrl: state === 'purged' ? null : input.urls?.(input.artifactId, release.releaseId).versionUrl ?? null };
33
33
  }));
34
34
  const last = page.at(-1);
35
35
  const position = (value) => ({ createdAt: value.createdAt, releaseId: value.releaseId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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",