@gradial/aci 0.1.52-rc.40 → 0.1.52-rc.41

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.52-rc.41
4
+
5
+ ### Changed
6
+
7
+ - Shared edge claims support collection-free external-audience delivery.
8
+ Personalization requires a recognized issuer marker; signed older cookies
9
+ remain valid evidence for consent withdrawal.
10
+
3
11
  ## 0.1.52-rc.40
4
12
 
5
13
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agentic Content Infrastructure SDK
2
2
 
3
- `@gradial/aci` `0.1.52-rc.40` is a pre-1.0 public-surface reset for the ACI
3
+ `@gradial/aci` `0.1.52-rc.41` is a pre-1.0 public-surface reset for the ACI
4
4
  client SDK. Breaking changes from the old `0.1.x` accidental surface are
5
5
  intentional: the root import is framework-neutral, framework runtime behavior is
6
6
  behind explicit subpaths, and the first required customer path is React/Next.
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install @gradial/aci@0.1.52-rc.40 zod@^4
11
+ npm install @gradial/aci@0.1.52-rc.41 zod@^4
12
12
  ```
13
13
 
14
14
  ## Root Import
@@ -137,13 +137,13 @@ host bundles select and serve immutable release artifacts.
137
137
  The first RC scaffold path is Next:
138
138
 
139
139
  ```bash
140
- npx @gradial/aci@0.1.52-rc.40 init my-site --template next-starter
140
+ npx @gradial/aci@0.1.52-rc.41 init my-site --template next-starter
141
141
  ```
142
142
 
143
143
  List available templates:
144
144
 
145
145
  ```bash
146
- npx @gradial/aci@0.1.52-rc.40 init --list
146
+ npx @gradial/aci@0.1.52-rc.41 init --list
147
147
  ```
148
148
 
149
149
  ## Personalization: The One-Tag Install
@@ -10,6 +10,47 @@ export declare function signIdentity(rawID: string, key: SigningKey): Promise<st
10
10
  export declare function randomIdentity(random?: (length: number) => Uint8Array): string;
11
11
  export declare function identityHashHex(namespace: string, value: string): Promise<string>;
12
12
  export declare function verifyClaims(value: string, keys: Record<string, string>, now: Date): Promise<Record<string, unknown> | null>;
13
+ export declare function verifyClaimsEvidence(value: string, keys: Record<string, string>, now: Date): Promise<Record<string, unknown> | null>;
14
+ /**
15
+ * The minting path that signed an aci_claims value: 'aci' claims contain
16
+ * first-party memberships only; 'edge' claims are the minted first-party ∪
17
+ * external union and require a matching live b2 receipt on a
18
+ * provider-enabled release.
19
+ */
20
+ export type ClaimsIssuer = 'aci' | 'edge';
21
+ /**
22
+ * Exposes the payload's `iss` marker. Malformed/unknown markers — and
23
+ * pre-0059 unmarked payloads — yield undefined; since the §6 cutover,
24
+ * verifyClaims rejects those payloads outright, so a verified payload always
25
+ * carries a marker. The marker identifies the minting path; it does not
26
+ * grant membership by itself.
27
+ */
28
+ export declare function claimsIssuer(payload: Record<string, unknown> | null): ClaimsIssuer | undefined;
29
+ /** audience.MaxClaimsCookieBytes — the hard 1 KiB aci_claims budget. */
30
+ export declare const MAX_CLAIMS_COOKIE_BYTES = 1024;
31
+ /** audience.DefaultClaimsTTL — the 24-hour signed-lifetime ceiling. */
32
+ export declare const DEFAULT_CLAIMS_TTL_SECONDS: number;
33
+ /**
34
+ * Mint rejection: oversize, a malformed key, or a missing issuer marker.
35
+ * Callers fail closed on it — the edge never truncates or subsets
36
+ * memberships to fit (ADR 0059 §2/§3).
37
+ */
38
+ export declare class ClaimsMintError extends Error {
39
+ readonly name = "ClaimsMintError";
40
+ }
41
+ /**
42
+ * Mints one signed aci_claims value: v1.{kid}.{base64url(payload)}.{hmac},
43
+ * byte-identical to Go's ClaimsIssuer.IssueClaims — payload field order is
44
+ * aud (omitted when empty), seg (omitted when empty), iat, exp, iss, and the
45
+ * TTL clamps to the 24-hour ceiling. The exact bytes are pinned against the
46
+ * Go golden in claims_contract.test.ts. Issuance requires an explicit issuer
47
+ * marker; the marker identifies the minting path, it does not grant
48
+ * membership by itself.
49
+ */
50
+ export declare function issueClaims(memberships: {
51
+ aud?: string[];
52
+ seg?: string[];
53
+ }, issuer: ClaimsIssuer, key: SigningKey, now: Date, ttlSeconds?: number): Promise<string>;
13
54
  export declare function verifyHMAC(value: string, signature: string, secret: string): Promise<boolean>;
14
55
  export declare function hmacSHA256(value: string, secret: string): Promise<string>;
15
56
  export declare function base64URLEncode(bytes: Uint8Array): string;
@@ -149,7 +149,30 @@ function bytesToHex(bytes) {
149
149
  return value;
150
150
  }
151
151
  // ── Claims cookie (aci_claims) ──
152
+ // verifyClaims is the PERSONALIZATION grade: signature, expiry, and the
153
+ // ADR 0059 §6 issuer-marker gate — a claim without a fresh marker is never
154
+ // trusted for membership selection. Existing signed cookies expire by their
155
+ // own TTL; their old contents are not read during the transition.
152
156
  export async function verifyClaims(value, keys, now) {
157
+ const payload = await verifyClaimsAuthenticity(value, keys, now);
158
+ if (payload === null || claimsIssuer(payload) === undefined)
159
+ return null;
160
+ return payload;
161
+ }
162
+ // verifyClaimsEvidence is the EVIDENCE grade (anonymous-experiments REQ-6):
163
+ // signature and expiry WITHOUT the issuer gate. The collector's
164
+ // withdrawal/suppression trigger only needs proof that identity-tier state
165
+ // existed — an authentically signed, unexpired pre-cutover (unmarked) cookie
166
+ // is exactly that proof, and the Go collector accepts it the same way (Go
167
+ // ClaimsVerifier.VerifyClaims has no marker gate). Evidence never feeds
168
+ // membership selection, so the §6 gate does not apply here.
169
+ export async function verifyClaimsEvidence(value, keys, now) {
170
+ return verifyClaimsAuthenticity(value, keys, now);
171
+ }
172
+ // verifyClaimsAuthenticity is the shared core: envelope, keyed signature,
173
+ // and expiry — no policy gate. Every structural failure degrades to null
174
+ // (never an error), matching Go's no-oracle posture.
175
+ async function verifyClaimsAuthenticity(value, keys, now) {
153
176
  const parts = value.trim().split('.');
154
177
  if (parts.length !== 4 || parts[0] !== 'v1')
155
178
  return null;
@@ -169,6 +192,67 @@ export async function verifyClaims(value, keys, now) {
169
192
  return null;
170
193
  }
171
194
  }
195
+ /**
196
+ * Exposes the payload's `iss` marker. Malformed/unknown markers — and
197
+ * pre-0059 unmarked payloads — yield undefined; since the §6 cutover,
198
+ * verifyClaims rejects those payloads outright, so a verified payload always
199
+ * carries a marker. The marker identifies the minting path; it does not
200
+ * grant membership by itself.
201
+ */
202
+ export function claimsIssuer(payload) {
203
+ const iss = payload?.iss;
204
+ return iss === 'aci' || iss === 'edge' ? iss : undefined;
205
+ }
206
+ /** audience.MaxClaimsCookieBytes — the hard 1 KiB aci_claims budget. */
207
+ export const MAX_CLAIMS_COOKIE_BYTES = 1024;
208
+ /** audience.DefaultClaimsTTL — the 24-hour signed-lifetime ceiling. */
209
+ export const DEFAULT_CLAIMS_TTL_SECONDS = 24 * 60 * 60;
210
+ /**
211
+ * Mint rejection: oversize, a malformed key, or a missing issuer marker.
212
+ * Callers fail closed on it — the edge never truncates or subsets
213
+ * memberships to fit (ADR 0059 §2/§3).
214
+ */
215
+ export class ClaimsMintError extends Error {
216
+ name = 'ClaimsMintError';
217
+ }
218
+ /**
219
+ * Mints one signed aci_claims value: v1.{kid}.{base64url(payload)}.{hmac},
220
+ * byte-identical to Go's ClaimsIssuer.IssueClaims — payload field order is
221
+ * aud (omitted when empty), seg (omitted when empty), iat, exp, iss, and the
222
+ * TTL clamps to the 24-hour ceiling. The exact bytes are pinned against the
223
+ * Go golden in claims_contract.test.ts. Issuance requires an explicit issuer
224
+ * marker; the marker identifies the minting path, it does not grant
225
+ * membership by itself.
226
+ */
227
+ export async function issueClaims(memberships, issuer, key, now, ttlSeconds) {
228
+ const kid = key.kid.trim();
229
+ if (!key.secret)
230
+ throw new ClaimsMintError('claims issuer requires a secret');
231
+ if (!kid)
232
+ throw new ClaimsMintError('claims issuer requires a kid');
233
+ if (kid.includes('.'))
234
+ throw new ClaimsMintError("claims kid must not contain '.'");
235
+ if (issuer !== 'aci' && issuer !== 'edge') {
236
+ throw new ClaimsMintError('claims issuer requires an explicit issuer marker');
237
+ }
238
+ const ttl = ttlSeconds === undefined || ttlSeconds <= 0 || ttlSeconds > DEFAULT_CLAIMS_TTL_SECONDS
239
+ ? DEFAULT_CLAIMS_TTL_SECONDS
240
+ : Math.floor(ttlSeconds);
241
+ const iat = Math.floor(now.getTime() / 1000);
242
+ const body = JSON.stringify({
243
+ ...(memberships.aud?.length ? { aud: memberships.aud } : {}),
244
+ ...(memberships.seg?.length ? { seg: memberships.seg } : {}),
245
+ iat,
246
+ exp: iat + ttl,
247
+ iss: issuer,
248
+ });
249
+ const encoded = base64URLEncode(encoder.encode(body));
250
+ const value = `v1.${kid}.${encoded}.${await hmacSHA256(`v1.${kid}.${encoded}`, key.secret)}`;
251
+ if (encoder.encode(value).byteLength > MAX_CLAIMS_COOKIE_BYTES) {
252
+ throw new ClaimsMintError(`claims cookie exceeds the ${MAX_CLAIMS_COOKIE_BYTES}-byte budget (aud=${memberships.aud?.length ?? 0} seg=${memberships.seg?.length ?? 0})`);
253
+ }
254
+ return value;
255
+ }
172
256
  // ── Internal crypto helpers ──
173
257
  // verifyHMAC returns a verdict about the signature, which is the only visitor
174
258
  // input here: a malformed or wrong signature is `false`. A fault importing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.52-rc.40",
3
+ "version": "0.1.52-rc.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/gradial/bare-metal-go.git",
@@ -160,11 +160,11 @@
160
160
  "zod": "^4.0.0"
161
161
  },
162
162
  "optionalDependencies": {
163
- "@gradial/aci-cli-darwin-arm64": "0.1.52-rc.40",
164
- "@gradial/aci-cli-darwin-x64": "0.1.52-rc.40",
165
- "@gradial/aci-cli-linux-arm64": "0.1.52-rc.40",
166
- "@gradial/aci-cli-linux-x64": "0.1.52-rc.40",
167
- "@gradial/aci-cli-win32-arm64": "0.1.52-rc.40",
168
- "@gradial/aci-cli-win32-x64": "0.1.52-rc.40"
163
+ "@gradial/aci-cli-darwin-arm64": "0.1.52-rc.41",
164
+ "@gradial/aci-cli-darwin-x64": "0.1.52-rc.41",
165
+ "@gradial/aci-cli-linux-arm64": "0.1.52-rc.41",
166
+ "@gradial/aci-cli-linux-x64": "0.1.52-rc.41",
167
+ "@gradial/aci-cli-win32-arm64": "0.1.52-rc.41",
168
+ "@gradial/aci-cli-win32-x64": "0.1.52-rc.41"
169
169
  }
170
170
  }