@atrib/attest 0.1.0 → 0.2.0

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
@@ -54,18 +54,20 @@ attest({
54
54
  provenance_token?: string,
55
55
  tool_name?: string,
56
56
  args_hash?: string,
57
+ args_salt?: string, // base64url 16-byte salt; requires args_hash
57
58
  result_hash?: string,
59
+ result_salt?: string, // base64url 16-byte salt; requires result_hash
58
60
  producer?: string, // sidecar label override; defaults to 'atrib-attest'
59
61
  })
60
62
  ```
61
63
 
62
64
  ### Ref mapping
63
65
 
64
- | `ref` argument | Signed record | Signed field carried |
65
- | --- | --- | --- |
66
- | absent | observation | none |
67
- | `{ kind: "annotates", target, ... }` | annotation | signed `annotates` field |
68
- | `{ kind: "revises", target, reason, ... }` (`reason` required) | revision | signed `revises` field |
66
+ | `ref` argument | Signed record | Signed field carried |
67
+ | -------------------------------------------------------------- | ------------- | ------------------------ |
68
+ | absent | observation | none |
69
+ | `{ kind: "annotates", target, ... }` | annotation | signed `annotates` field |
70
+ | `{ kind: "revises", target, reason, ... }` (`reason` required) | revision | signed `revises` field |
69
71
 
70
72
  The three shapes route through the same `handleEmit` funnel used by the
71
73
  legacy tools. There is no separate signing path for `attest`: the ref
@@ -110,10 +112,10 @@ re-export shims.
110
112
 
111
113
  ```typescript
112
114
  import {
113
- createAtribAttestServer, // mounts the four-tool write union
114
- createAtribEmitServer, // legacy factory; also mounts attest
115
- createAtribAnnotateServer, // legacy factory; also mounts attest
116
- createAtribReviseServer, // legacy factory; also mounts attest
115
+ createAtribAttestServer, // mounts the four-tool write union
116
+ createAtribEmitServer, // legacy factory; also mounts attest
117
+ createAtribAnnotateServer, // legacy factory; also mounts attest
118
+ createAtribReviseServer, // legacy factory; also mounts attest
117
119
  handleAttest,
118
120
  attestInProcess,
119
121
 
@@ -123,7 +125,7 @@ import {
123
125
  EmitInput,
124
126
  resolveKey,
125
127
  emitSessionCheckpoint,
126
- } from "@atrib/attest";
128
+ } from '@atrib/attest'
127
129
  ```
128
130
 
129
131
  Each legacy factory (`createAtribEmitServer`, `createAtribAnnotateServer`,
@@ -160,6 +162,11 @@ verb that contract is the most important thing to know:
160
162
  carries only the replay-checkable `args_hash` commitment over it. The
161
163
  public log stores the 90-byte commitment entry (record hash, creator key,
162
164
  context_id, timestamp, event_type byte), never the content.
165
+ - **Salted direct commitments.** `args_salt` and `result_salt` let direct
166
+ attest callers carry the same salted commitment posture as middleware.
167
+ Each salt must be canonical base64url for exactly 16 bytes and requires its
168
+ matching explicit hash. The shared `@atrib/mcp` commitment helper computes
169
+ and verifies the bytes used by both paths.
163
170
  - **Refused writes are loud, not silent.** A malformed call (unknown
164
171
  `ref.kind`, missing revises reason, a `ref`/content contradiction, a
165
172
  provenance_token off genesis, or no resolvable signing key) returns
package/dist/attest.d.ts CHANGED
@@ -24,7 +24,9 @@ export declare const AttestInput: z.ZodObject<{
24
24
  provenance_token: z.ZodOptional<z.ZodString>;
25
25
  tool_name: z.ZodOptional<z.ZodString>;
26
26
  args_hash: z.ZodOptional<z.ZodString>;
27
+ args_salt: z.ZodOptional<z.ZodString>;
27
28
  result_hash: z.ZodOptional<z.ZodString>;
29
+ result_salt: z.ZodOptional<z.ZodString>;
28
30
  producer: z.ZodOptional<z.ZodString>;
29
31
  }, z.core.$strip>;
30
32
  export type AttestInputT = z.infer<typeof AttestInput>;
@@ -46,7 +48,9 @@ export interface MappedAttestInput {
46
48
  revises?: string;
47
49
  tool_name?: string;
48
50
  args_hash?: string;
51
+ args_salt?: string;
49
52
  result_hash?: string;
53
+ result_salt?: string;
50
54
  };
51
55
  }
52
56
  export interface AttestMappingRefusal {
package/dist/attest.js CHANGED
@@ -16,19 +16,29 @@
16
16
  // (matching what atrib-annotate / atrib-revise compose today), so the
17
17
  // D099 default args_hash commitment is byte-identical across both names.
18
18
  import { z } from 'zod';
19
- import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, SHA256_REF_PATTERN, } from '@atrib/mcp';
19
+ import { EVENT_TYPE_ANNOTATION_URI, EVENT_TYPE_REVISION_URI, SHA256_REF_PATTERN } from '@atrib/mcp';
20
20
  const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
21
21
  // 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
22
22
  const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
23
23
  const OBSERVATION_URI = 'https://atrib.dev/v1/types/observation';
24
24
  export const AttestRef = z.object({
25
- kind: z.enum(['annotates', 'revises']).describe("Declared-relationship kind. 'annotates' marks a past record's importance " +
25
+ kind: z
26
+ .enum(['annotates', 'revises'])
27
+ .describe("Declared-relationship kind. 'annotates' marks a past record's importance " +
26
28
  "and meaning (spec §1.2.7 / D058); 'revises' supersedes a prior position " +
27
29
  'with a stated reason (spec §1.2.9 / D059). Omit `ref` entirely for a ' +
28
30
  'plain observation.'),
29
- target: z.string().regex(SHA256_REF_PATTERN).describe("'sha256:<64-hex>' record_hash the relationship points at. The target can " +
31
+ target: z
32
+ .string()
33
+ .regex(SHA256_REF_PATTERN)
34
+ .describe("'sha256:<64-hex>' record_hash the relationship points at. The target can " +
30
35
  "be any prior record (yours or another agent's)."),
31
- reason: z.string().min(1).max(4096).optional().describe("Why the position changed. REQUIRED when kind is 'revises'; composed into " +
36
+ reason: z
37
+ .string()
38
+ .min(1)
39
+ .max(4096)
40
+ .optional()
41
+ .describe("Why the position changed. REQUIRED when kind is 'revises'; composed into " +
32
42
  'the content body exactly as atrib-revise composes it today.'),
33
43
  });
34
44
  export const AttestInput = z.object({
@@ -85,11 +95,21 @@ export const AttestInput = z.object({
85
95
  .optional()
86
96
  .describe('Optional §8.3 args_hash commitment override. When omitted, attest signs ' +
87
97
  'sha256(JCS(content)) per D099, identical to the legacy write names.'),
98
+ args_salt: z
99
+ .string()
100
+ .regex(/^[A-Za-z0-9_-]{22}$/)
101
+ .optional()
102
+ .describe('Optional base64url 16-byte salt paired with args_hash per §8.3.'),
88
103
  result_hash: z
89
104
  .string()
90
105
  .regex(SHA256_REF_PATTERN)
91
106
  .optional()
92
107
  .describe('Optional §8.3 result_hash commitment, unchanged from the legacy emit tool.'),
108
+ result_salt: z
109
+ .string()
110
+ .regex(/^[A-Za-z0-9_-]{22}$/)
111
+ .optional()
112
+ .describe('Optional base64url 16-byte salt paired with result_hash per §8.3.'),
93
113
  producer: z
94
114
  .string()
95
115
  .min(1)
@@ -122,7 +142,9 @@ export function mapAttestInput(input) {
122
142
  ...(input.provenance_token ? { provenance_token: input.provenance_token } : {}),
123
143
  ...(input.tool_name ? { tool_name: input.tool_name } : {}),
124
144
  ...(input.args_hash ? { args_hash: input.args_hash } : {}),
145
+ ...(input.args_salt ? { args_salt: input.args_salt } : {}),
125
146
  ...(input.result_hash ? { result_hash: input.result_hash } : {}),
147
+ ...(input.result_salt ? { result_salt: input.result_salt } : {}),
126
148
  };
127
149
  if (!input.ref) {
128
150
  const conflict = contentRefConflicts(input.content, undefined);
@@ -185,7 +207,7 @@ function contentRefConflicts(content, ref) {
185
207
  if (!ref) {
186
208
  if (contentAnnotates !== undefined || contentRevises !== undefined) {
187
209
  return ('content carries a relationship field (annotates/revises) but no ref was declared; ' +
188
- "declare ref: { kind, target } instead of embedding the relationship in content");
210
+ 'declare ref: { kind, target } instead of embedding the relationship in content');
189
211
  }
190
212
  return null;
191
213
  }
@@ -205,9 +227,7 @@ function contentRefConflicts(content, ref) {
205
227
  if (contentRevises !== undefined && contentRevises !== ref.target) {
206
228
  return `content.revises (${String(contentRevises)}) contradicts ref.target (${ref.target})`;
207
229
  }
208
- if (contentReason !== undefined &&
209
- ref.reason !== undefined &&
210
- contentReason !== ref.reason) {
230
+ if (contentReason !== undefined && ref.reason !== undefined && contentReason !== ref.reason) {
211
231
  return 'content.reason contradicts ref.reason';
212
232
  }
213
233
  return null;
package/dist/index.d.ts CHANGED
@@ -18,7 +18,9 @@ declare const EmitInput: z.ZodObject<{
18
18
  revises: z.ZodOptional<z.ZodString>;
19
19
  tool_name: z.ZodOptional<z.ZodString>;
20
20
  args_hash: z.ZodOptional<z.ZodString>;
21
+ args_salt: z.ZodOptional<z.ZodString>;
21
22
  result_hash: z.ZodOptional<z.ZodString>;
23
+ result_salt: z.ZodOptional<z.ZodString>;
22
24
  }, z.core.$strip>;
23
25
  type EmitSignedOutput = {
24
26
  signed: true;
@@ -308,5 +310,5 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
308
310
  export type { AttestInputT, MappedAttestInput, AttestMappingRefusal } from './attest.js';
309
311
  export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
310
312
  export type { AtribAnnotateServer, CreateAtribAnnotateServerOptions } from './annotate.js';
311
- export { ReviseInput, createAtribReviseServer, registerReviseTool, } from './revise.js';
313
+ export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
312
314
  export type { AtribReviseServer, CreateAtribReviseServerOptions } from './revise.js';
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import { resolveKey } from './keys.js';
24
24
  import { filterResolvableInformedBy } from './reference-resolution.js';
25
25
  import { buildAndSignEmitRecord } from './sign.js';
26
26
  import { mirrorRecord } from './storage.js';
27
- import { AttestInput, isAttestMappingRefusal, mapAttestInput, } from './attest.js';
27
+ import { AttestInput, isAttestMappingRefusal, mapAttestInput } from './attest.js';
28
28
  import { registerAnnotateTool } from './annotate.js';
29
29
  import { registerReviseTool } from './revise.js';
30
30
  // Read-side mirror inheritance: ATRIB_AUTOCHAIN_SOURCE points at the file
@@ -41,6 +41,7 @@ function readMirrorPath() {
41
41
  const HEX_32_PATTERN = /^[0-9a-f]{32}$/;
42
42
  // 16 bytes encoded as base64url with no padding = 22 chars per spec §1.2.6.
43
43
  const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
44
+ const COMMITMENT_SALT_PATTERN = /^[A-Za-z0-9_-]{22}$/;
44
45
  const DEFAULT_KEY_RESOLVE_RETRY_MS = 30_000;
45
46
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
46
47
  function keyResolveRetryMs() {
@@ -138,17 +139,24 @@ const EmitInput = z.object({
138
139
  .optional()
139
140
  .describe('Optional §8.3 args_hash commitment. Format: "sha256:" + 64 lowercase hex. Lets ' +
140
141
  'emit-signed records carry a commitment to canonical args bytes for downstream ' +
141
- 'consumers (e.g. recall filtering by args_hash, or replay detection). Salted vs ' +
142
- 'plain forms hash identically on the wire; the salt (when used) is carried in the ' +
143
- 'separate args_salt field, which this surface does not yet expose.'),
142
+ 'consumers (e.g. recall filtering by args_hash, or replay detection).'),
143
+ args_salt: z
144
+ .string()
145
+ .regex(COMMITMENT_SALT_PATTERN)
146
+ .optional()
147
+ .describe('Optional base64url 16-byte salt paired with args_hash per §8.3.'),
144
148
  result_hash: z
145
149
  .string()
146
150
  .regex(SHA256_REF_PATTERN)
147
151
  .optional()
148
152
  .describe('Optional §8.3 result_hash commitment. Format: "sha256:" + 64 lowercase hex. Lets ' +
149
153
  'emit-signed records carry a commitment to canonical result bytes for downstream ' +
150
- 'consumers. Salted vs plain forms hash identically on the wire; the salt (when used) ' +
151
- 'is carried in the separate result_salt field, which this surface does not yet expose.'),
154
+ 'consumers.'),
155
+ result_salt: z
156
+ .string()
157
+ .regex(COMMITMENT_SALT_PATTERN)
158
+ .optional()
159
+ .describe('Optional base64url 16-byte salt paired with result_hash per §8.3.'),
152
160
  });
153
161
  const DEFAULT_LOCAL_SUBSTRATE_DEGRADATION = {
154
162
  if_unavailable: 'sign locally in producer and continue without coordinator receipt',
@@ -212,7 +220,7 @@ export function registerEmitTool(mcp, deps) {
212
220
  */
213
221
  export function registerAttestTool(mcp, deps) {
214
222
  mcp.registerTool('attest', {
215
- description: 'Make a signed statement now: atrib\'s write verb. Signs an observation by default; ' +
223
+ description: "Make a signed statement now: atrib's write verb. Signs an observation by default; " +
216
224
  'declare ref: { kind: "annotates", target } to mark a past record\'s importance, or ' +
217
225
  'ref: { kind: "revises", target, reason } to supersede a prior position. One handler ' +
218
226
  'behind the legacy emit / atrib-annotate / atrib-revise names; records are ' +
@@ -475,11 +483,15 @@ async function handleEmit({ input, key, queue, producer, logEndpoint, recordRefe
475
483
  revises: input.revises,
476
484
  toolName: input.tool_name,
477
485
  argsHash: input.args_hash,
486
+ argsSalt: input.args_salt,
478
487
  resultHash: input.result_hash,
488
+ resultSalt: input.result_salt,
479
489
  });
480
490
  }
481
491
  catch (e) {
482
- return refusalOutput(contextId, [`signing failed: ${e instanceof Error ? e.message : String(e)}`]);
492
+ return refusalOutput(contextId, [
493
+ `signing failed: ${e instanceof Error ? e.message : String(e)}`,
494
+ ]);
483
495
  }
484
496
  const recordHash = hashRecord(record);
485
497
  const unsignedRecordBody = unsignedRecordBodyFromSigned(record);
@@ -1025,4 +1037,4 @@ export { AttestInput, AttestRef, mapAttestInput, isAttestMappingRefusal } from '
1025
1037
  // Legacy specialized writers, kept as permanent aliases over the attest
1026
1038
  // funnel. The @atrib/annotate and @atrib/revise packages re-export these.
1027
1039
  export { AnnotateInput, Importance, createAtribAnnotateServer, registerAnnotateTool, } from './annotate.js';
1028
- export { ReviseInput, createAtribReviseServer, registerReviseTool, } from './revise.js';
1040
+ export { ReviseInput, createAtribReviseServer, registerReviseTool } from './revise.js';
package/dist/sign.d.ts CHANGED
@@ -46,11 +46,15 @@ export interface BuildEmitRecordInput {
46
46
  * replay-checkable without exposing the body to the public log.
47
47
  */
48
48
  argsHash?: string | undefined;
49
+ /** Optional 16-byte base64url salt paired with argsHash. */
50
+ argsSalt?: string | undefined;
49
51
  /**
50
52
  * Optional §8.3 result_hash commitment. Same posture semantics and wire
51
53
  * format as args_hash, but commits to the tool result bytes.
52
54
  */
53
55
  resultHash?: string | undefined;
56
+ /** Optional 16-byte base64url salt paired with resultHash. */
57
+ resultSalt?: string | undefined;
54
58
  }
55
59
  /**
56
60
  * Build, sign, and return a complete AtribRecord ready for submission.
package/dist/sign.js CHANGED
@@ -23,6 +23,12 @@ const encoder = new TextEncoder();
23
23
  * Pure aside from the signing primitive itself; no network I/O here.
24
24
  */
25
25
  export async function buildAndSignEmitRecord(input) {
26
+ if (input.argsSalt && !input.argsHash) {
27
+ throw new TypeError('args_salt requires an explicit args_hash');
28
+ }
29
+ if (input.resultSalt && !input.resultHash) {
30
+ throw new TypeError('result_salt requires result_hash');
31
+ }
26
32
  const publicKey = base64urlEncode(await getPublicKey(input.privateKey));
27
33
  const toolName = leafOfEventTypeUri(input.eventType);
28
34
  const contentId = computeContentId(SYNTHETIC_SERVER_URL, toolName);
@@ -30,9 +36,7 @@ export async function buildAndSignEmitRecord(input) {
30
36
  // informed_by must be sorted lexicographically per §1.2.5 to keep the
31
37
  // canonical form stable across emitters. Omitted entirely (not null,
32
38
  // not empty) when no references are given, presence affects JCS.
33
- const informedBySorted = input.informedBy && input.informedBy.length > 0
34
- ? [...input.informedBy].sort()
35
- : undefined;
39
+ const informedBySorted = input.informedBy && input.informedBy.length > 0 ? [...input.informedBy].sort() : undefined;
36
40
  const record = {
37
41
  spec_version: 'atrib/1.0',
38
42
  content_id: contentId,
@@ -45,8 +49,10 @@ export async function buildAndSignEmitRecord(input) {
45
49
  ...(informedBySorted ? { informed_by: informedBySorted } : {}),
46
50
  ...(input.annotates ? { annotates: input.annotates } : {}),
47
51
  ...(argsHash ? { args_hash: argsHash } : {}),
52
+ ...(input.argsSalt ? { args_salt: input.argsSalt } : {}),
48
53
  ...(input.provenanceToken ? { provenance_token: input.provenanceToken } : {}),
49
54
  ...(input.resultHash ? { result_hash: input.resultHash } : {}),
55
+ ...(input.resultSalt ? { result_salt: input.resultSalt } : {}),
50
56
  ...(input.revises ? { revises: input.revises } : {}),
51
57
  ...(input.toolName ? { tool_name: input.toolName } : {}),
52
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atrib/attest",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for atrib's write verb. One attest tool signs observations, annotations, and revisions; the legacy emit, atrib-annotate, and atrib-revise tool names stay mounted as aliases over the same handler.",
5
5
  "keywords": [
6
6
  "atrib",
@@ -45,13 +45,13 @@
45
45
  "@noble/hashes": "^2.2.0",
46
46
  "canonicalize": "^3.0.0",
47
47
  "zod": "^3.25.76",
48
- "@atrib/mcp": "0.21.0"
48
+ "@atrib/mcp": "0.22.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@types/node": "^25.9.3",
52
- "tsx": "^4.22.4",
51
+ "@types/node": "^26.1.1",
52
+ "tsx": "^4.23.1",
53
53
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.8"
54
+ "vitest": "^4.1.10"
55
55
  },
56
56
  "files": [
57
57
  "dist"