@sentropic/artifacts 0.2.6 → 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 +39 -0
- package/README.md +19 -11
- package/dist/content/handler.js +33 -22
- package/dist/content/routing.d.ts +2 -5
- package/dist/content/routing.js +7 -11
- package/dist/http.d.ts +3 -3
- package/dist/http.js +7 -4
- package/dist/mcp-core.d.ts +1 -1
- package/dist/mcp-core.js +5 -2
- package/dist/profile/schemas.js +4 -1
- package/dist/server/mime.d.ts +7 -0
- package/dist/server/mime.js +7 -0
- package/dist/server/result-schema.js +10 -2
- package/dist/server/source-read.d.ts +25 -5
- package/dist/server/source-read.js +17 -2
- package/dist/server/summary.d.ts +2 -2
- package/dist/server/summary.js +1 -1
- package/dist/server/templates.d.ts +5 -0
- package/dist/server/templates.js +7 -1
- package/dist/server/versions.d.ts +2 -2
- package/dist/server/versions.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
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
|
+
|
|
24
|
+
## 0.2.7
|
|
25
|
+
|
|
26
|
+
- Fix the `artifacts_get_source` wire payload that broke page editing in claude.ai:
|
|
27
|
+
files are no longer returned as a JSON integer array (`bytes: Array.from(bytes)`).
|
|
28
|
+
Text mime types (the canonical `TEXT_MIME_PATTERN`) come back as `text` with
|
|
29
|
+
`encoding: "utf8"`; every other file — including non-UTF-8 "text" — as `base64`
|
|
30
|
+
with `encoding: "base64"`. Size and digest verification still run on the raw
|
|
31
|
+
bytes before encoding. An 816-byte page now serializes to a few KiB instead of
|
|
32
|
+
blowing the tool-result budget.
|
|
33
|
+
- `get_source` (and the conflict `currentSource`) no longer embeds any authoring
|
|
34
|
+
guide; the result carries only a `guideRef` (`{tool: "artifacts_get_api_guide",
|
|
35
|
+
version}`) naming the tool and version to fetch it from.
|
|
36
|
+
- Per-call budgets are unchanged: at most 32 files and 262144 decoded bytes; the
|
|
37
|
+
2 MiB serialized MCP result budget still holds for the full snapshot.
|
|
38
|
+
- Tool schema (`artifacts_get_source` description, v2 result schema source files),
|
|
39
|
+
the API guide text and the README source section describe the text/base64
|
|
40
|
+
encoding and the guide reference.
|
|
41
|
+
|
|
3
42
|
## 0.2.6
|
|
4
43
|
|
|
5
44
|
- Integrate part 1 with the source reads, sharing, terminal deletion, masking and
|
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` —
|
|
390
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
|
@@ -439,9 +443,13 @@ empty defaults. Existing profile selections remain explicit.
|
|
|
439
443
|
|
|
440
444
|
`POST /get_source` accepts `artifactId`, optional `releaseId`, and optional unique
|
|
441
445
|
`paths` (1–32 entries). Bind `publication.getRelease` and `sources.getSource` on
|
|
442
|
-
the HTTP app. It returns the canonical manifest, verified author
|
|
443
|
-
|
|
444
|
-
|
|
446
|
+
the HTTP app. It returns the canonical manifest, verified author files, and a
|
|
447
|
+
separate nullable `sourceRef`; readers need the distinct source permission.
|
|
448
|
+
Size and digest are verified on the raw bytes, then each file is encoded for the
|
|
449
|
+
wire: text mime types as `text` with `encoding: "utf8"`, all other files as
|
|
450
|
+
`base64` with `encoding: "base64"` — never an integer array. The result carries
|
|
451
|
+
a `guideRef` (`{tool, version}`) instead of the guide body. The snapshot is
|
|
452
|
+
limited to 32 files and 262144 decoded bytes. Hosts must qualify
|
|
445
453
|
a 2 MiB serialized MCP result budget before selecting this capability.
|
|
446
454
|
|
|
447
455
|
`POST /set_access` grants or revokes a named principal's read access, requires a
|
package/dist/content/handler.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L-5d — transport-free content handler (
|
|
3
|
-
* release bytes on the separate content domain (never HTML on
|
|
4
|
-
* control origin).
|
|
5
|
-
*
|
|
6
|
-
* release
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
let channel;
|
|
83
|
+
if (current !== undefined) {
|
|
84
|
+
artifactId = origin.id;
|
|
85
|
+
release = current;
|
|
86
|
+
channel = 'stable';
|
|
73
87
|
}
|
|
74
|
-
else {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
3
|
-
|
|
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;
|
package/dist/content/routing.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L-5d — content routing (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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: '
|
|
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
|
-
|
|
57
|
+
versionUrl: string;
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
export interface ArtifactApp {
|
|
@@ -64,7 +64,7 @@ export interface ArtifactApp {
|
|
|
64
64
|
handle(request: HttpRequest, invocation: ArtifactInvocationContext): Promise<HttpResponse>;
|
|
65
65
|
}
|
|
66
66
|
/** Retained for consumers describing canonical textual publication inputs. */
|
|
67
|
-
export
|
|
67
|
+
export { TEXT_MIME_PATTERN } from './server/mime.js';
|
|
68
68
|
/** Other mutation surfaces need their own qualification before host activation. */
|
|
69
69
|
export declare function assertArtifactsTransportProfile(value: unknown): asserts value is CompiledArtifactsProfile;
|
|
70
70
|
export declare function createHttpArtifactApp(deps: HttpAppDeps): ArtifactApp;
|
package/dist/http.js
CHANGED
|
@@ -15,6 +15,8 @@ import { artifactSummary } from './server/summary.js';
|
|
|
15
15
|
import { artifactVersions } from './server/versions.js';
|
|
16
16
|
import { manageArtifact } from './server/management.js';
|
|
17
17
|
import { createPostgresManagementUnit } from './adapters/postgres-management.js';
|
|
18
|
+
import { TEXT_MIME_RE } from './server/mime.js';
|
|
19
|
+
import { API_GUIDE_REF } from './server/templates.js';
|
|
18
20
|
export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
19
21
|
/** Stable content-route error codes; private driver messages never leave the host. */
|
|
20
22
|
export function errorBody(status) {
|
|
@@ -23,8 +25,7 @@ export function errorBody(status) {
|
|
|
23
25
|
500: 'internal_error', 503: 'capability_unavailable' }[status] ?? 'internal_error' };
|
|
24
26
|
}
|
|
25
27
|
/** Retained for consumers describing canonical textual publication inputs. */
|
|
26
|
-
export
|
|
27
|
-
const TEXT_MIME_RE = new RegExp(TEXT_MIME_PATTERN);
|
|
28
|
+
export { TEXT_MIME_PATTERN } from './server/mime.js';
|
|
28
29
|
function object(value) {
|
|
29
30
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
30
31
|
throw new ArtifactProfileInputError();
|
|
@@ -160,11 +161,13 @@ export function createHttpArtifactApp(deps) {
|
|
|
160
161
|
const files = (await readManifestFiles(release.manifest, invocation.storageScopeId)).filter(file => paths.has(file.path))
|
|
161
162
|
.map(file => ({ path: file.path, mimeType: file.mimeType, size: file.bytes.byteLength,
|
|
162
163
|
digest: entries.find(entry => entry.path === file.path).digest,
|
|
163
|
-
...(file.text === undefined
|
|
164
|
+
...(file.text === undefined
|
|
165
|
+
? { base64: Buffer.from(file.bytes).toString('base64'), encoding: 'base64' }
|
|
166
|
+
: { text: file.text, encoding: 'utf8' }) }));
|
|
164
167
|
await visible(artifactId, invocation, 'source');
|
|
165
168
|
const failure = { ...outcome.failure, conflict: { ...conflict, currentSource: {
|
|
166
169
|
artifactId, releaseId: release.releaseId, digest: conflict.currentDigest, manifest: release.manifest,
|
|
167
|
-
files, sourceRef: null, ...deps.publicationUrls?.(artifactId, release.releaseId),
|
|
170
|
+
files, sourceRef: null, guideRef: API_GUIDE_REF, ...deps.publicationUrls?.(artifactId, release.releaseId),
|
|
168
171
|
} } };
|
|
169
172
|
Object.defineProperty(failure, 'cause', { value: outcome.failure.cause, enumerable: false });
|
|
170
173
|
return { ok: false, failure: Object.freeze(failure) };
|
package/dist/mcp-core.d.ts
CHANGED
|
@@ -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.
|
|
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,10 +7,13 @@ 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.
|
|
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
|
-
description:
|
|
13
|
+
description: name === 'artifacts_get_source'
|
|
14
|
+
? 'Artifacts get source: verified files of the current or a selected release as `text` (encoding "utf8") '
|
|
15
|
+
+ 'or `base64` (encoding "base64"), never an integer array, plus a guideRef instead of the guide body.'
|
|
16
|
+
: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
|
|
14
17
|
mutability: (['artifacts_list', 'artifacts_get', 'artifacts_get_template', 'artifacts_get_api_guide',
|
|
15
18
|
'artifacts_get_source', 'artifacts_list_versions'].includes(name) ? 'read-only' : name === 'artifacts_publish' ? 'append' : 'state-transition'),
|
|
16
19
|
}));
|
package/dist/profile/schemas.js
CHANGED
|
@@ -86,7 +86,10 @@ export function capabilitySchema(name, profile) {
|
|
|
86
86
|
case 'list_versions': return profile.resultEnvelopeVersion !== 2 ? idOnly : obj({ artifactId: id,
|
|
87
87
|
limit: { ...integer(1, 100), default: 20 }, cursor: { ...str, maxLength: 4096 } }, ['artifactId']);
|
|
88
88
|
case 'get_source': return obj({ artifactId: id, releaseId: id,
|
|
89
|
-
paths: { type: 'array', minItems: 1, maxItems: 32, items: { ...str, maxLength: profile.rules.pathMaxBytes }
|
|
89
|
+
paths: { type: 'array', minItems: 1, maxItems: 32, items: { ...str, maxLength: profile.rules.pathMaxBytes },
|
|
90
|
+
description: 'Unique manifest paths; omit to read the whole snapshot (max 32 files, 256 KiB decoded). ' +
|
|
91
|
+
'Result files are `text` with encoding "utf8" or `base64` with encoding "base64" (never an integer ' +
|
|
92
|
+
'array), plus a guideRef; the guide body is never embedded.' } }, ['artifactId']);
|
|
90
93
|
case 'delete': return obj({ artifactId: id, expectedCurrentReleaseId: { ...id, type: ['string', 'null'] },
|
|
91
94
|
idempotencyKey: key, confirmationRef: { ...str, maxLength: 4096 } }, ['artifactId', 'idempotencyKey', ...(profile.requireExpectedCurrentReleaseId ? ['expectedCurrentReleaseId'] : [])]);
|
|
92
95
|
case 'revert': {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical text-mime pattern shared by publication input rules and source
|
|
3
|
+
* reads. Leaf module: both `http.ts` and `server/source-read.ts` import it
|
|
4
|
+
* without an import cycle.
|
|
5
|
+
*/
|
|
6
|
+
export declare const TEXT_MIME_PATTERN = "^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])";
|
|
7
|
+
export declare const TEXT_MIME_RE: RegExp;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical text-mime pattern shared by publication input rules and source
|
|
3
|
+
* reads. Leaf module: both `http.ts` and `server/source-read.ts` import it
|
|
4
|
+
* without an import cycle.
|
|
5
|
+
*/
|
|
6
|
+
export const TEXT_MIME_PATTERN = '^(?:text/[a-z0-9!#$&^_.+-]+|application/(?:javascript|ecmascript|json|xml|[a-z0-9!#$&^_.+-]+\\+(?:json|xml))|image/svg\\+xml)(?![\\s\\S])';
|
|
7
|
+
export const TEXT_MIME_RE = new RegExp(TEXT_MIME_PATTERN);
|
|
@@ -7,11 +7,19 @@ const object = (properties, required = Object.keys(properties)) => ({
|
|
|
7
7
|
const error = (code, details) => object({
|
|
8
8
|
code: { enum: [code] }, message: string, retriable: { type: 'boolean' }, details,
|
|
9
9
|
});
|
|
10
|
+
/** get_source file: verified raw bytes encoded as UTF-8 text or base64, never an integer array. */
|
|
11
|
+
const sourceFile = { type: 'object', additionalProperties: false,
|
|
12
|
+
properties: { path: string, mimeType: string, size: { type: 'integer', minimum: 0 }, digest: string,
|
|
13
|
+
encoding: { enum: ['utf8', 'base64'] }, text: string, base64: string },
|
|
14
|
+
required: ['path', 'mimeType', 'size', 'digest', 'encoding'],
|
|
15
|
+
oneOf: [{ required: ['text'], not: { required: ['base64'] } },
|
|
16
|
+
{ required: ['base64'], not: { required: ['text'] } }] };
|
|
17
|
+
const guideRef = object({ tool: string, version: string }, ['tool', 'version']);
|
|
10
18
|
const pointer = object({ variant: { enum: ['pointer_conflict'] }, artifactId: string,
|
|
11
19
|
expectedCurrentReleaseId: nullable, currentReleaseId: nullable, currentDigest: nullable,
|
|
12
20
|
currentSource: object({ artifactId: string, releaseId: string, digest: nullable,
|
|
13
|
-
manifest: string, files: { type: 'array', items:
|
|
14
|
-
stableUrl: string,
|
|
21
|
+
manifest: string, files: { type: 'array', items: sourceFile }, sourceRef: { type: ['object', 'null'] },
|
|
22
|
+
guideRef, stableUrl: string, versionUrl: string }, ['artifactId', 'releaseId', 'digest', 'manifest', 'files', 'sourceRef', 'guideRef']),
|
|
15
23
|
}, ['variant', 'artifactId', 'expectedCurrentReleaseId', 'currentReleaseId', 'currentDigest']);
|
|
16
24
|
const common = { invocationId: string,
|
|
17
25
|
commitState: { enum: ['not_committed', 'committed', 'unknown'] } };
|
|
@@ -9,7 +9,12 @@ export interface SourceReadPorts {
|
|
|
9
9
|
getBlob(key: string): Promise<Uint8Array | undefined>;
|
|
10
10
|
urls?: (artifactId: string, releaseId: string) => {
|
|
11
11
|
stableUrl: string;
|
|
12
|
-
|
|
12
|
+
versionUrl: string;
|
|
13
|
+
};
|
|
14
|
+
/** Guide reference returned instead of embedding the guide body. */
|
|
15
|
+
guideRef?: {
|
|
16
|
+
tool: string;
|
|
17
|
+
version: string;
|
|
13
18
|
};
|
|
14
19
|
maxFiles: number;
|
|
15
20
|
maxTotalBytes: number;
|
|
@@ -22,17 +27,32 @@ export interface SourceReadInput {
|
|
|
22
27
|
/** Read author bytes only. Provenance never substitutes for a stored snapshot. */
|
|
23
28
|
export declare function readArtifactSource(ports: SourceReadPorts, input: SourceReadInput): Promise<{
|
|
24
29
|
stableUrl?: string | undefined;
|
|
25
|
-
|
|
30
|
+
versionUrl?: string | undefined;
|
|
26
31
|
artifactId: string;
|
|
27
32
|
releaseId: string;
|
|
28
33
|
digest: string;
|
|
29
34
|
manifest: string;
|
|
30
|
-
files: {
|
|
31
|
-
|
|
35
|
+
files: ({
|
|
36
|
+
text: string;
|
|
37
|
+
encoding: "utf8";
|
|
38
|
+
path: string;
|
|
39
|
+
mimeType: string;
|
|
40
|
+
digest: string;
|
|
41
|
+
size: number;
|
|
42
|
+
} | {
|
|
43
|
+
base64: string;
|
|
44
|
+
encoding: "base64";
|
|
32
45
|
path: string;
|
|
33
46
|
mimeType: string;
|
|
34
47
|
digest: string;
|
|
35
48
|
size: number;
|
|
36
|
-
}[];
|
|
49
|
+
})[];
|
|
37
50
|
sourceRef: SourceRef | null;
|
|
51
|
+
guideRef: {
|
|
52
|
+
readonly tool: "artifacts_get_api_guide";
|
|
53
|
+
readonly version: "v1";
|
|
54
|
+
} | {
|
|
55
|
+
tool: string;
|
|
56
|
+
version: string;
|
|
57
|
+
};
|
|
38
58
|
}>;
|
|
@@ -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
|
-
|
|
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,
|
|
81
|
+
manifest: release.manifest, files, sourceRef, guideRef: ports.guideRef ?? API_GUIDE_REF,
|
|
82
|
+
...ports.urls?.(input.artifactId, release.releaseId) };
|
|
68
83
|
}
|
package/dist/server/summary.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function artifactSummary(input: {
|
|
|
10
10
|
};
|
|
11
11
|
urls?: (artifactId: string, releaseId: string) => {
|
|
12
12
|
stableUrl: string;
|
|
13
|
-
|
|
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
|
-
|
|
36
|
+
versionUrl: string | null;
|
|
37
37
|
title: string;
|
|
38
38
|
description: string;
|
|
39
39
|
icon: string | null;
|
package/dist/server/summary.js
CHANGED
|
@@ -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,
|
|
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,
|
|
@@ -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;
|
package/dist/server/templates.js
CHANGED
|
@@ -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">',
|
|
@@ -66,7 +68,11 @@ const GUIDE_V1 = [
|
|
|
66
68
|
'- `artifacts_list_versions`: full release history of one artifact —',
|
|
67
69
|
' per release: id, created at, author, state, whether it is current,',
|
|
68
70
|
' and a content digest.',
|
|
69
|
-
'- `artifacts_get_source`:
|
|
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.',
|
|
70
76
|
'- `artifacts_revert`: make an earlier release current again as a NEW',
|
|
71
77
|
' release that copies its content — the artifact URL stays stable and',
|
|
72
78
|
' history only grows. Purged releases are refused with `release_unavailable`.',
|
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
+
versionUrl: string | null;
|
|
27
27
|
}[];
|
|
28
28
|
nextCursor: string | null;
|
|
29
29
|
}>;
|
package/dist/server/versions.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|