@hasna/events 0.1.14 → 0.1.16

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/dist/storage.js CHANGED
@@ -304,17 +304,17 @@ function statusFile(dataDir, fileName, records) {
304
304
  return { path, exists: existsSync(path), records };
305
305
  }
306
306
  export {
307
- normalizeEventPageLimit,
308
- localJsonRuntime,
309
- getEventsStatus,
310
- getEventsDataDir,
311
- getActiveEventsDirEnv,
312
- encodeLocalJsonEventCursor,
313
- decodeLocalJsonEventCursor,
314
- MAX_EVENT_PAGE_LIMIT,
315
- LOCAL_JSON_EVENT_CURSOR_PREFIX,
316
- JsonEventsStore,
317
- HASNA_EVENTS_HOME_ENV,
307
+ DEFAULT_EVENT_PAGE_LIMIT,
318
308
  HASNA_EVENTS_DIR_ENV,
319
- DEFAULT_EVENT_PAGE_LIMIT
309
+ HASNA_EVENTS_HOME_ENV,
310
+ JsonEventsStore,
311
+ LOCAL_JSON_EVENT_CURSOR_PREFIX,
312
+ MAX_EVENT_PAGE_LIMIT,
313
+ decodeLocalJsonEventCursor,
314
+ encodeLocalJsonEventCursor,
315
+ getActiveEventsDirEnv,
316
+ getEventsDataDir,
317
+ getEventsStatus,
318
+ localJsonRuntime,
319
+ normalizeEventPageLimit
320
320
  };
@@ -42,21 +42,27 @@ function now() {
42
42
  function truncate(value, max = 4096) {
43
43
  return value.length > max ? `${value.slice(0, max)}...` : value;
44
44
  }
45
- function buildWebhookRequest(event, channel) {
45
+ function buildWebhookRequest(event, channel, options = {}) {
46
46
  if (!channel.webhook)
47
47
  throw new Error(`Channel ${channel.id} has no webhook config`);
48
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
49
+ if (/^x-hasna-/i.test(name)) {
50
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
51
+ }
52
+ }
48
53
  const body = JSON.stringify(event);
49
- const timestamp = event.time;
54
+ const timestamp = options.timestamp ?? new Date().toISOString();
50
55
  const headers = {
51
56
  "Content-Type": "application/json",
52
57
  "User-Agent": "@hasna/events",
53
58
  "X-Hasna-Event-Id": event.id,
54
59
  "X-Hasna-Event-Type": event.type,
55
- "X-Hasna-Timestamp": timestamp,
56
- ...channel.webhook.headers
60
+ ...channel.webhook.headers,
61
+ "X-Hasna-Timestamp": timestamp
57
62
  };
58
- if (channel.webhook.secret) {
59
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
63
+ const secret = options.secret ?? channel.webhook.secret;
64
+ if (secret) {
65
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
60
66
  }
61
67
  return { body, headers };
62
68
  }
@@ -64,7 +70,21 @@ async function dispatchWebhook(event, channel, options = {}) {
64
70
  if (!channel.webhook)
65
71
  throw new Error(`Channel ${channel.id} has no webhook config`);
66
72
  const startedAt = now();
67
- const { body, headers } = buildWebhookRequest(event, channel);
73
+ let secret = channel.webhook.secret;
74
+ if (channel.webhook.secretRef) {
75
+ if (!options.secretResolver) {
76
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
77
+ }
78
+ try {
79
+ secret = await options.secretResolver(channel.webhook.secretRef);
80
+ } catch {
81
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
82
+ }
83
+ if (!secret)
84
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
85
+ }
86
+ const timestamp = (options.now?.() ?? new Date).toISOString();
87
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
68
88
  const controller = new AbortController;
69
89
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
70
90
  try {
@@ -96,6 +116,15 @@ async function dispatchWebhook(event, channel, options = {}) {
96
116
  clearTimeout(timeout);
97
117
  }
98
118
  }
119
+ function failedAttempt(startedAt, error) {
120
+ return {
121
+ attempt: 1,
122
+ status: "failed",
123
+ startedAt,
124
+ completedAt: now(),
125
+ error
126
+ };
127
+ }
99
128
  async function dispatchCommand(event, channel) {
100
129
  if (!channel.command)
101
130
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -185,9 +214,9 @@ function createDeliveryResult(event, channel, attempts) {
185
214
  };
186
215
  }
187
216
  export {
188
- dispatchWebhook,
189
- dispatchCommand,
190
- dispatchChannel,
217
+ buildWebhookRequest,
191
218
  createDeliveryResult,
192
- buildWebhookRequest
219
+ dispatchChannel,
220
+ dispatchCommand,
221
+ dispatchWebhook
193
222
  };
@@ -0,0 +1,108 @@
1
+ {
2
+ "event_id": "evt_fixture_todo_completed_01",
3
+ "event_type": "todos.task.completed",
4
+ "schema_version": "hasna.app_event.v1",
5
+ "source": {
6
+ "app": "todos",
7
+ "version": "0.0.0-fixture",
8
+ "machine": "fixture.local"
9
+ },
10
+ "occurred_at": "2026-07-29T12:00:00.000Z",
11
+ "severity": "notice",
12
+ "idempotency": {
13
+ "dedupe_key": "fixture:interop-project:todo-42:completed:v1",
14
+ "replay_safe": true
15
+ },
16
+ "correlation": {
17
+ "correlation_id": "corr_fixture_interop_project_01",
18
+ "causation_id": "evt_fixture_todo_created_01"
19
+ },
20
+ "subject": {
21
+ "kind": "task",
22
+ "id": "todo_fixture_42",
23
+ "uri": "hasna://todos/tasks/todo_fixture_42"
24
+ },
25
+ "actor": {
26
+ "kind": "service",
27
+ "id": "todos",
28
+ "name": "Todos fixture producer"
29
+ },
30
+ "project_mappings": {
31
+ "canonical_id": "project_fixture_interop",
32
+ "slug": "interop-fixture",
33
+ "repository": "https://example.invalid/hasna/interop-fixture",
34
+ "workspace": "workspace_fixture_interop",
35
+ "external_ids": {
36
+ "todos": "project_fixture_interop",
37
+ "conversations": "conversation_fixture_17",
38
+ "mementos": "memento_fixture_09",
39
+ "projects": "project_fixture_interop",
40
+ "codewith": "mailbox_fixture_interop"
41
+ }
42
+ },
43
+ "summary": "Interop fixture task completed successfully.",
44
+ "data": {
45
+ "task_status": "completed",
46
+ "task_title": "Verify app-event interoperability",
47
+ "conversation_id": "conversation_fixture_17",
48
+ "memento_id": "memento_fixture_09",
49
+ "mailbox_topic": "project.activity",
50
+ "render_hint": {
51
+ "component": "StatusBadge",
52
+ "tone": "success"
53
+ }
54
+ },
55
+ "resource_refs": [
56
+ {
57
+ "kind": "task",
58
+ "id": "todo_fixture_42",
59
+ "uri": "hasna://todos/tasks/todo_fixture_42",
60
+ "source_package": "@hasna/todos",
61
+ "external_id": "todo_fixture_42"
62
+ },
63
+ {
64
+ "kind": "conversation",
65
+ "id": "conversation_fixture_17",
66
+ "uri": "hasna://conversations/conversation_fixture_17"
67
+ },
68
+ {
69
+ "kind": "memento",
70
+ "id": "memento_fixture_09",
71
+ "uri": "hasna://mementos/memento_fixture_09"
72
+ },
73
+ {
74
+ "kind": "render",
75
+ "id": "project_fixture_interop_status",
76
+ "uri": "hasna://projects/project_fixture_interop/render/status"
77
+ }
78
+ ],
79
+ "evidence_refs": [
80
+ {
81
+ "kind": "test_result",
82
+ "id": "evidence_fixture_interop_01",
83
+ "uri": "artifact://interop-fixture/test-report.json",
84
+ "redaction": "none"
85
+ }
86
+ ],
87
+ "sensitivity": {
88
+ "classification": "internal",
89
+ "contains_personal_data": false
90
+ },
91
+ "redaction": {
92
+ "state": "none",
93
+ "fields": [],
94
+ "safe_for_logs": true
95
+ },
96
+ "delivery": {
97
+ "intent": "notification",
98
+ "mode": "at_least_once",
99
+ "targets": [
100
+ "todos",
101
+ "conversations",
102
+ "mementos",
103
+ "projects-json-render",
104
+ "codewith-mailbox"
105
+ ],
106
+ "agent_conversation_injection": false
107
+ }
108
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "schema": "hasna.service_contract.v1",
3
+ "name": "events",
4
+ "class": "library",
5
+ "contractVersion": "v1",
6
+ "kitVersion": "0.8.5",
7
+ "description": "Shared event envelopes, local channels, replay, and delivery transports for Hasna open-source apps. Library class: SDK exports are the primary surface and the local `events` CLI is the operator seam. The default compatibility store remains user-owned JSON; opt-in durable delivery uses a Node-safe filesystem spool plus a Bun SQLite outbox under the same caller-selected data directory. The package provisions no remote infrastructure.",
8
+ "bins": [
9
+ "events"
10
+ ],
11
+ "hosting": [
12
+ "user-hosted"
13
+ ],
14
+ "serviceSurfaces": [
15
+ {
16
+ "name": "package-sdk",
17
+ "kind": "sdk",
18
+ "status": "supported",
19
+ "authMode": "none",
20
+ "exportSubpath": "."
21
+ },
22
+ {
23
+ "name": "durable-spool-sdk",
24
+ "kind": "sdk",
25
+ "status": "supported",
26
+ "authMode": "none",
27
+ "exportSubpath": "./durable-spool"
28
+ },
29
+ {
30
+ "name": "durable-delivery-sdk",
31
+ "kind": "sdk",
32
+ "status": "supported",
33
+ "authMode": "none",
34
+ "exportSubpath": "./durable"
35
+ },
36
+ {
37
+ "name": "durable-worker-sdk",
38
+ "kind": "sdk",
39
+ "status": "supported",
40
+ "authMode": "none",
41
+ "exportSubpath": "./durable-worker"
42
+ },
43
+ {
44
+ "name": "events-cli",
45
+ "kind": "cli",
46
+ "status": "supported",
47
+ "bin": "events",
48
+ "authMode": "local-only"
49
+ }
50
+ ],
51
+ "metadata": {
52
+ "release": {
53
+ "artifactScan": {
54
+ "script": "artifact-scan"
55
+ }
56
+ },
57
+ "conformance": {
58
+ "waivedSurfaces": [
59
+ {
60
+ "kind": "api",
61
+ "reason": "Embedded library plus a local CLI; it owns no HTTP service boundary and ships no serve bin."
62
+ },
63
+ {
64
+ "kind": "mcp",
65
+ "reason": "Embedded library; MCP execution belongs to the applications that consume these envelopes."
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ }
package/package.json CHANGED
@@ -1,57 +1,85 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Shared event envelopes, local channels, replay, and delivery transports for Hasna open-source apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
7
+ "types": "./types/index.d.ts",
8
8
  "bin": {
9
9
  "events": "dist/cli/index.js",
10
10
  "hasna-events": "dist/cli/index.js"
11
11
  },
12
12
  "exports": {
13
13
  ".": {
14
- "types": "./dist/index.d.ts",
14
+ "types": "./types/index.d.ts",
15
15
  "import": "./dist/index.js"
16
16
  },
17
17
  "./storage": {
18
- "types": "./dist/storage.d.ts",
18
+ "types": "./types/storage.d.ts",
19
19
  "import": "./dist/storage.js"
20
20
  },
21
+ "./durable": {
22
+ "types": "./types/durable.d.ts",
23
+ "import": "./dist/durable.js"
24
+ },
25
+ "./durable-spool": {
26
+ "types": "./types/durable-spool.d.ts",
27
+ "import": "./dist/durable-spool.js"
28
+ },
29
+ "./durable-worker": {
30
+ "types": "./types/durable-worker.d.ts",
31
+ "import": "./dist/durable-worker.js"
32
+ },
21
33
  "./signing": {
22
- "types": "./dist/signing.d.ts",
34
+ "types": "./types/signing.d.ts",
23
35
  "import": "./dist/signing.js"
24
36
  },
25
37
  "./filter": {
26
- "types": "./dist/filter.d.ts",
38
+ "types": "./types/filter.d.ts",
27
39
  "import": "./dist/filter.js"
28
40
  },
29
41
  "./transports": {
30
- "types": "./dist/transports.d.ts",
42
+ "types": "./types/transports.d.ts",
31
43
  "import": "./dist/transports.js"
32
44
  },
33
45
  "./catalog": {
34
- "types": "./dist/catalog.d.ts",
46
+ "types": "./types/catalog.d.ts",
35
47
  "import": "./dist/catalog.js"
36
48
  },
49
+ "./app-event": {
50
+ "types": "./types/app-event.d.ts",
51
+ "import": "./dist/app-event.js"
52
+ },
53
+ "./schemas/hasna.app_event.v1.json": "./schemas/hasna.app_event.v1.json",
54
+ "./fixtures/hasna.app_event.v1.json": "./fixtures/hasna.app_event.v1.json",
37
55
  "./commander": {
38
- "types": "./dist/commander.d.ts",
56
+ "types": "./types/commander.d.ts",
39
57
  "import": "./dist/commander.js"
40
58
  },
41
59
  "./cli": {
42
- "types": "./dist/cli/index.d.ts",
60
+ "types": "./types/cli/index.d.ts",
43
61
  "import": "./dist/cli/index.js"
44
62
  }
45
63
  },
46
64
  "files": [
47
65
  "dist",
66
+ "types",
67
+ "schemas",
68
+ "fixtures",
48
69
  "README.md",
49
- "LICENSE"
70
+ "LICENSE",
71
+ "hasna.contract.json"
50
72
  ],
51
73
  "scripts": {
52
- "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/index.ts src/storage.ts src/signing.ts src/filter.ts src/transports.ts src/types.ts src/commander.ts src/catalog.ts --outdir dist --target bun && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
74
+ "build": "bun run build:runtime && bun run build:types",
75
+ "build:runtime": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun && bun build src/index.ts src/storage.ts src/signing.ts src/filter.ts src/transports.ts src/types.ts src/commander.ts src/catalog.ts src/app-event.ts src/durable.ts src/durable-worker.ts --root src --outdir dist --target bun && bun build src/durable-spool.ts --root src --outdir dist --target node",
76
+ "build:types": "rm -rf types && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir types",
77
+ "generated-artifacts:check": "bun run build && test -z \"$(git status --porcelain --untracked-files=all -- dist types)\"",
78
+ "contract:check": "contracts repo-conformance .",
79
+ "artifact-scan": "bun scripts/artifact-scan.ts",
53
80
  "typecheck": "tsc --noEmit",
54
81
  "test": "bun test",
82
+ "prepack": "bun run build && bun run artifact-scan",
55
83
  "prepublishOnly": "bun run test && bun run build"
56
84
  },
57
85
  "keywords": [
@@ -75,7 +103,8 @@
75
103
  "url": "https://github.com/hasna/events/issues"
76
104
  },
77
105
  "engines": {
78
- "bun": ">=1.0.0"
106
+ "bun": ">=1.0.0",
107
+ "node": ">=20.0.0"
79
108
  },
80
109
  "author": "Hasna",
81
110
  "license": "Apache-2.0",
@@ -83,6 +112,7 @@
83
112
  "commander": "^13.1.0"
84
113
  },
85
114
  "devDependencies": {
115
+ "@hasna/contracts": "0.8.5",
86
116
  "@types/bun": "latest",
87
117
  "typescript": "^5.7.3"
88
118
  }
@@ -0,0 +1,186 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.hasna.com/hasna.app_event.v1.json",
4
+ "title": "hasna.app_event.v1",
5
+ "description": "Versioned cross-app event profile carried by @hasna/events.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "event_id",
10
+ "event_type",
11
+ "schema_version",
12
+ "source",
13
+ "occurred_at",
14
+ "severity",
15
+ "idempotency",
16
+ "correlation",
17
+ "subject",
18
+ "actor",
19
+ "project_mappings",
20
+ "summary",
21
+ "data",
22
+ "resource_refs",
23
+ "evidence_refs",
24
+ "sensitivity",
25
+ "redaction",
26
+ "delivery"
27
+ ],
28
+ "properties": {
29
+ "event_id": { "type": "string", "minLength": 1, "maxLength": 200 },
30
+ "event_type": { "type": "string", "minLength": 1, "maxLength": 200 },
31
+ "schema_version": { "const": "hasna.app_event.v1" },
32
+ "source": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "required": ["app", "version", "machine"],
36
+ "properties": {
37
+ "app": { "type": "string", "minLength": 1, "maxLength": 200 },
38
+ "version": { "type": "string", "minLength": 1, "maxLength": 100 },
39
+ "machine": { "type": "string", "minLength": 1, "maxLength": 200 }
40
+ }
41
+ },
42
+ "occurred_at": { "type": "string", "format": "date-time" },
43
+ "severity": { "enum": ["debug", "info", "notice", "warning", "error", "critical"] },
44
+ "idempotency": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "required": ["dedupe_key", "replay_safe"],
48
+ "properties": {
49
+ "dedupe_key": { "type": "string", "minLength": 1, "maxLength": 512 },
50
+ "replay_safe": { "type": "boolean" },
51
+ "replay_of_event_id": { "type": "string", "minLength": 1, "maxLength": 200 }
52
+ }
53
+ },
54
+ "correlation": {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["correlation_id"],
58
+ "properties": {
59
+ "correlation_id": { "type": "string", "minLength": 1, "maxLength": 200 },
60
+ "causation_id": { "type": "string", "minLength": 1, "maxLength": 200 },
61
+ "trace_id": { "type": "string", "minLength": 1, "maxLength": 200 }
62
+ }
63
+ },
64
+ "subject": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": ["kind", "id"],
68
+ "properties": {
69
+ "kind": { "type": "string", "minLength": 1, "maxLength": 100 },
70
+ "id": { "type": "string", "minLength": 1, "maxLength": 200 },
71
+ "uri": { "type": "string", "minLength": 1, "maxLength": 2048, "format": "uri-reference" }
72
+ }
73
+ },
74
+ "actor": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["kind", "id"],
78
+ "properties": {
79
+ "kind": { "enum": ["agent", "human", "service", "model", "workflow", "system"] },
80
+ "id": { "type": "string", "minLength": 1, "maxLength": 200 },
81
+ "name": { "type": "string", "minLength": 1, "maxLength": 200 }
82
+ }
83
+ },
84
+ "project_mappings": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "required": ["canonical_id", "external_ids"],
88
+ "properties": {
89
+ "canonical_id": { "type": "string", "minLength": 1, "maxLength": 200 },
90
+ "slug": { "type": "string", "minLength": 1, "maxLength": 200 },
91
+ "repository": { "type": "string", "minLength": 1, "maxLength": 2048, "format": "uri-reference" },
92
+ "workspace": { "type": "string", "minLength": 1, "maxLength": 2048 },
93
+ "external_ids": {
94
+ "type": "object",
95
+ "maxProperties": 16,
96
+ "propertyNames": { "minLength": 1 },
97
+ "additionalProperties": { "type": "string", "minLength": 1 }
98
+ }
99
+ }
100
+ },
101
+ "summary": { "type": "string", "minLength": 1, "maxLength": 512 },
102
+ "data": {
103
+ "type": "object",
104
+ "description": "Bounded to 32768 serialized UTF-8 bytes by the runtime validator.",
105
+ "x-max-utf8-bytes": 32768
106
+ },
107
+ "resource_refs": {
108
+ "type": "array",
109
+ "maxItems": 32,
110
+ "items": { "$ref": "#/$defs/resource_ref" }
111
+ },
112
+ "evidence_refs": {
113
+ "type": "array",
114
+ "maxItems": 32,
115
+ "items": { "$ref": "#/$defs/evidence_ref" }
116
+ },
117
+ "sensitivity": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "required": ["classification", "contains_personal_data"],
121
+ "properties": {
122
+ "classification": { "enum": ["public", "internal", "confidential", "restricted"] },
123
+ "contains_personal_data": { "type": "boolean" }
124
+ }
125
+ },
126
+ "redaction": {
127
+ "type": "object",
128
+ "additionalProperties": false,
129
+ "required": ["state", "fields", "safe_for_logs"],
130
+ "properties": {
131
+ "state": { "enum": ["none", "partial", "full"] },
132
+ "fields": {
133
+ "type": "array",
134
+ "maxItems": 32,
135
+ "items": { "type": "string", "minLength": 1 }
136
+ },
137
+ "safe_for_logs": { "type": "boolean" }
138
+ }
139
+ },
140
+ "delivery": {
141
+ "type": "object",
142
+ "additionalProperties": false,
143
+ "required": ["intent", "mode", "targets", "agent_conversation_injection"],
144
+ "properties": {
145
+ "intent": { "enum": ["notification", "state_sync", "audit", "command"] },
146
+ "mode": { "enum": ["at_most_once", "at_least_once"] },
147
+ "targets": {
148
+ "type": "array",
149
+ "minItems": 1,
150
+ "maxItems": 16,
151
+ "items": { "type": "string", "minLength": 1 }
152
+ },
153
+ "agent_conversation_injection": {
154
+ "const": false,
155
+ "description": "Event text is untrusted data and must not be injected into agent conversations."
156
+ }
157
+ }
158
+ }
159
+ },
160
+ "$defs": {
161
+ "resource_ref": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": ["kind", "id"],
165
+ "properties": {
166
+ "kind": { "type": "string", "minLength": 1, "maxLength": 100 },
167
+ "id": { "type": "string", "minLength": 1, "maxLength": 200 },
168
+ "uri": { "type": "string", "minLength": 1, "maxLength": 2048, "format": "uri-reference" },
169
+ "source_package": { "type": "string", "minLength": 1, "maxLength": 200 },
170
+ "external_id": { "type": "string", "minLength": 1, "maxLength": 200 }
171
+ }
172
+ },
173
+ "evidence_ref": {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "required": ["kind", "id", "uri", "redaction"],
177
+ "properties": {
178
+ "kind": { "type": "string", "minLength": 1, "maxLength": 100 },
179
+ "id": { "type": "string", "minLength": 1, "maxLength": 200 },
180
+ "uri": { "type": "string", "minLength": 1, "maxLength": 2048, "format": "uri-reference" },
181
+ "sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" },
182
+ "redaction": { "enum": ["none", "partial", "full"] }
183
+ }
184
+ }
185
+ }
186
+ }