@sentropic/artifacts 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @sentropic/artifacts — generic artifact contracts (L-5a)
2
2
 
3
3
  Generic, host-independent artifact domain state for the Sentropic artifacts
4
- provider (`sentropic.artifacts`, spec-v8 §9). Single unreleased `0.1.0`
5
- candidate covering L-5a–5e; no publication before the final merge.
4
+ provider (`sentropic.artifacts`, spec-v8 §9), covering L-5a–5e. Version
5
+ `0.1.1` fixes activation ticket release and channel binding (FL-ACT).
6
6
 
7
7
  ## What's here (L-5a)
8
8
 
@@ -51,7 +51,8 @@ No Immo defaults, URLs or bindings in generic source.
51
51
  ## What's here (L-5d–5e)
52
52
 
53
53
  - `src/content/grants.ts` — 55s HMAC content grants (60s cutoff budget) and
54
- single-use bound activation tickets; secrets and clocks are injected.
54
+ single-use activation tickets bound to subject, artifact, exact release
55
+ and channel (`stable` or `pinned`); secrets and clocks are injected.
55
56
  - `src/content/routing.ts` — stable (`<artifactId>.<base>`) and pinned
56
57
  (`<releaseId>.pinned.<base>`) single-UUID origins; safe path resolution.
57
58
  - `src/content/headers.ts` — no-store/nosniff/frame-denial/CSP headers and
@@ -68,11 +69,19 @@ No Immo defaults, URLs or bindings in generic source.
68
69
  `@sentropic/mcp-platform` kernel registry definition (the platform is
69
70
  an optional peer, required only by the `./mcp` subpath).
70
71
 
72
+ Ticket issuance and redemption both require `subjectId`, `artifactId`,
73
+ `releaseId` and `channel`. The handler checks the release resolved from the
74
+ requested origin and its channel before minting a grant. A stable ticket
75
+ issued for r1 cannot activate r2 after republish, and a stable ticket cannot
76
+ activate a pinned origin (or vice versa). Mismatches return 401 without a
77
+ grant cookie and preserve the ticket for a matching request. Tickets expire
78
+ at `expiresAt` (120 seconds by default) and can be redeemed only once.
79
+
71
80
  ## First publish (bootstrap)
72
81
 
73
- `0.1.0` is the single candidate covering L-5a–5e; it is not on npm yet,
74
- so the steady-state OIDC job cannot publish before an npm trusted
75
- publisher exists. Bootstrap sequence (owner-run, never automated here):
82
+ The steady-state OIDC job requires an npm trusted publisher. For the first
83
+ publication, use the current package version and this bootstrap sequence
84
+ (owner-run, never automated here):
76
85
 
77
86
  1. Trigger the `bootstrap-publish` workflow manually with
78
87
  `bootstrap_publish_target=artifacts` (runs
@@ -24,11 +24,12 @@ export declare function createHmacGrantSigner(options: {
24
24
  clock?: () => number;
25
25
  ttlSeconds?: number;
26
26
  }): GrantSigner;
27
- export interface TicketRecord {
27
+ export interface TicketBinding extends GrantIssueInput {
28
+ releaseId: string;
29
+ channel: 'stable' | 'pinned';
30
+ }
31
+ export interface TicketRecord extends TicketBinding {
28
32
  ticketId: string;
29
- subjectId: string;
30
- artifactId: string;
31
- releaseId?: string;
32
33
  expiresAt: number;
33
34
  }
34
35
  export interface TicketStore {
@@ -41,14 +42,11 @@ export interface TicketStore {
41
42
  /** In-memory TicketStore double for unit tests. */
42
43
  export declare function createMemoryTicketStore(): TicketStore;
43
44
  export interface TicketService {
44
- issue(input: GrantIssueInput): Promise<{
45
+ issue(input: TicketBinding): Promise<{
45
46
  ticketId: string;
46
47
  expiresAt: number;
47
48
  }>;
48
- redeem(ticketId: string, expected: {
49
- subjectId: string;
50
- artifactId: string;
51
- }): Promise<TicketRecord>;
49
+ redeem(ticketId: string, expected: TicketBinding): Promise<TicketRecord>;
52
50
  }
53
51
  /** Single-use bound tickets; mismatched attempts do not consume the ticket. */
54
52
  export declare function createTicketService(options: {
@@ -7,7 +7,7 @@
7
7
  * host-injected secret, bound to subject + artifact (+ pinned release),
8
8
  * and verified fail-closed (signature, shape, lifetime cap, expiry,
9
9
  * future-issued beyond the skew budget). Activation tickets are
10
- * single-use and bound the same way; ticket IDs come from an injected
10
+ * single-use and bound to an exact release and channel; ticket IDs come from an injected
11
11
  * port and MUST be unguessable (default: crypto UUIDs). A grant never
12
12
  * replaces the per-request ACL recheck in the content handler.
13
13
  */
@@ -101,15 +101,23 @@ export function createTicketService(options) {
101
101
  const clock = options.clock ?? (() => Date.now());
102
102
  const ids = options.ids ?? (() => crypto.randomUUID());
103
103
  const ttlMs = (options.ttlSeconds ?? CONTENT_TICKET_TTL_SECONDS) * 1000;
104
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0)
105
+ throw new Error('invalid ticket: lifetime');
104
106
  return {
105
107
  async issue(input) {
108
+ if (typeof input.subjectId !== 'string' || input.subjectId.length === 0 ||
109
+ typeof input.artifactId !== 'string' || input.artifactId.length === 0 ||
110
+ typeof input.releaseId !== 'string' || input.releaseId.length === 0 ||
111
+ (input.channel !== 'stable' && input.channel !== 'pinned'))
112
+ throw new Error('invalid ticket: binding');
106
113
  const ticketId = ids();
107
114
  const expiresAt = clock() + ttlMs;
108
115
  await options.store.put({
109
116
  ticketId,
110
117
  subjectId: input.subjectId,
111
118
  artifactId: input.artifactId,
112
- ...(input.releaseId === undefined ? {} : { releaseId: input.releaseId }),
119
+ releaseId: input.releaseId,
120
+ channel: input.channel,
113
121
  expiresAt,
114
122
  });
115
123
  return { ticketId, expiresAt };
@@ -118,11 +126,12 @@ export function createTicketService(options) {
118
126
  const record = await options.store.peek(ticketId);
119
127
  if (record === undefined)
120
128
  throw new Error('invalid ticket');
121
- if (clock() > record.expiresAt) {
129
+ if (clock() >= record.expiresAt) {
122
130
  await options.store.remove(ticketId);
123
131
  throw new Error('ticket expired');
124
132
  }
125
- if (record.subjectId !== expected.subjectId || record.artifactId !== expected.artifactId) {
133
+ if (record.subjectId !== expected.subjectId || record.artifactId !== expected.artifactId ||
134
+ record.releaseId !== expected.releaseId || record.channel !== expected.channel) {
126
135
  throw new Error('invalid ticket: binding mismatch');
127
136
  }
128
137
  const taken = await options.store.take(ticketId);
@@ -80,7 +80,9 @@ export function createContentHandler(deps) {
80
80
  if (request.actorId === undefined || request.actorId.length === 0)
81
81
  return fail(401);
82
82
  try {
83
- await deps.tickets.redeem(request.ticket, { subjectId: request.actorId, artifactId });
83
+ await deps.tickets.redeem(request.ticket, {
84
+ subjectId: request.actorId, artifactId, releaseId: release.releaseId, channel: origin.kind,
85
+ });
84
86
  }
85
87
  catch {
86
88
  return fail(401);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sentropic/artifacts",
3
- "version": "0.1.0",
4
- "description": "Generic configurable artifacts MCP and artifact service: StorageScope/Artifact/Binding/Release model, canonical manifests, object authorization, idempotent intents (spec-v8 §9). Single 0.1.0 candidate for L-5a–5e; unpublished.",
3
+ "version": "0.1.1",
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",
7
7
  "repository": {