chainlesschain 0.166.140 → 0.166.142

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.
@@ -0,0 +1,212 @@
1
+ /** A separately consented v4 lease observes bounded native evidence snapshots. */
2
+ import path from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { createHash } from "node:crypto";
6
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
7
+ import { managedSettingsPath } from "../settings-loader.cjs";
8
+ import { createProjectGoalConnection } from "../project-goal-connection.js";
9
+ import {
10
+ collectGoalExtensions,
11
+ projectGoalExtensionEvent,
12
+ } from "./goal-extensions.js";
13
+
14
+ const TYPES = ["risk.review.completed", "action.receipt.updated"];
15
+ const SCHEMA = "chainlesschain.goal-extension-stream/v1";
16
+ export const _deps = { createProjectGoalConnection, projectGoalExtensionEvent };
17
+ const fail = (code) => {
18
+ throw new Error(code);
19
+ };
20
+ const digest = (value) =>
21
+ createHash("sha256").update(JSON.stringify(value)).digest("hex");
22
+
23
+ /** Capture options before any I/O. Never reinterpret another lease as events. */
24
+ export function watchCliProjectGoalEvents(id, options = {}) {
25
+ const cwd = path.resolve(options.cwd || process.cwd());
26
+ const env = Object.freeze({ ...(options.env || process.env) });
27
+ const managedSettingsFile = managedSettingsPath({
28
+ env,
29
+ managedSettingsFile: options.managedSettingsFile,
30
+ });
31
+ const name = options.plugin,
32
+ scope = options.scope,
33
+ expectedStore = options.expectedStore;
34
+ const connectionFile = options.connection;
35
+ const intervalMs = options.intervalMs ?? 1000,
36
+ durationMs = options.durationMs ?? 60000;
37
+ const signal = options.signal;
38
+ if (typeof name !== "string" || !name || name.length > 256)
39
+ fail("CLI_GOAL_INVALID_PLUGIN_NAME");
40
+ if (scope !== undefined && !["user", "project", "local"].includes(scope))
41
+ fail("CLI_GOAL_INVALID_PLUGIN_SCOPE");
42
+ if (
43
+ !Number.isSafeInteger(intervalMs) ||
44
+ intervalMs < 250 ||
45
+ intervalMs > 60000 ||
46
+ !Number.isSafeInteger(durationMs) ||
47
+ durationMs < 1000 ||
48
+ durationMs > 900000
49
+ )
50
+ fail("CLI_GOAL_INVALID_WATCH_LIMIT");
51
+ if (signal !== undefined && !(signal instanceof AbortSignal))
52
+ fail("CLI_GOAL_INVALID_WATCH_SIGNAL");
53
+ const host = cliGoalExtensionHost(TYPES.map((type) => `event:${type}`));
54
+ const admission = Object.freeze({ cwd, env, managedSettingsFile, host });
55
+ return (async function* () {
56
+ if (signal?.aborted) return;
57
+ const source = _deps.createProjectGoalConnection(connectionFile);
58
+ if (!source.events || typeof source.readEvents !== "function")
59
+ fail("CLI_GOAL_EVENT_CONNECTION_REQUIRED");
60
+ const goalRef = source.goalRef;
61
+ if (
62
+ id !== goalRef.id ||
63
+ (expectedStore !== undefined && expectedStore !== goalRef.storeId)
64
+ )
65
+ fail("CLI_GOAL_STORE_MISMATCH");
66
+ const matches = collectGoalExtensions(admission).extensions.filter(
67
+ (entry) =>
68
+ entry.name === name &&
69
+ (!scope || entry.scope === scope) &&
70
+ entry.declaration.events.some((type) => TYPES.includes(type)),
71
+ );
72
+ if (matches.length !== 1) fail("CLI_GOAL_EVENT_PLUGIN_UNAVAILABLE");
73
+ const plugin = matches[0];
74
+ const ref = Object.freeze({
75
+ name: plugin.name,
76
+ scope: plugin.scope,
77
+ version: plugin.version,
78
+ });
79
+ const declaration = digest(plugin.declaration);
80
+ function revalidatePlugin() {
81
+ const current = collectGoalExtensions(admission).extensions.find(
82
+ (entry) =>
83
+ entry.name === ref.name &&
84
+ entry.scope === ref.scope &&
85
+ entry.version === ref.version,
86
+ );
87
+ if (!current || digest(current.declaration) !== declaration)
88
+ fail("CLI_GOAL_EVENT_ADMISSION_CHANGED");
89
+ }
90
+ const deadline = performance.now() + durationMs;
91
+ const controller = new AbortController();
92
+ const stop = () => controller.abort();
93
+ const timer = setTimeout(stop, durationMs);
94
+ timer.unref();
95
+ signal?.addEventListener("abort", stop, { once: true });
96
+ if (signal?.aborted) stop();
97
+ async function read() {
98
+ revalidatePlugin();
99
+ const value = await source.readEvents(id, { signal: controller.signal });
100
+ revalidatePlugin();
101
+ return value;
102
+ }
103
+ async function revalidate(snapshot) {
104
+ if (digest(await read()) !== digest(snapshot))
105
+ fail("CLI_GOAL_EVENT_SOURCE_CHANGED");
106
+ }
107
+ try {
108
+ let previous = await read();
109
+ await revalidate(previous);
110
+ if (controller.signal.aborted) return;
111
+ yield Object.freeze({
112
+ schema: SCHEMA,
113
+ kind: "ready",
114
+ source: "desktop-project",
115
+ plugin: ref,
116
+ goalRef,
117
+ observedAt: new Date().toISOString(),
118
+ delivery: "polling",
119
+ replay: false,
120
+ });
121
+ while (!controller.signal.aborted && performance.now() < deadline) {
122
+ await delay(
123
+ Math.min(intervalMs, Math.max(1, deadline - performance.now())),
124
+ undefined,
125
+ { signal: controller.signal },
126
+ );
127
+ if (controller.signal.aborted || performance.now() >= deadline) return;
128
+ const current = await read();
129
+ if (digest(current) === digest(previous)) continue;
130
+ const events = [];
131
+ if (
132
+ (previous.risk.reviewId && !current.risk.reviewId) ||
133
+ (previous.receipts.state === "recorded" &&
134
+ current.receipts.state !== "recorded")
135
+ )
136
+ fail("CLI_GOAL_EVENT_SOURCE_REGRESSED");
137
+ if (
138
+ current.risk.reviewId &&
139
+ digest(current.risk) !== digest(previous.risk) &&
140
+ plugin.declaration.events.includes(TYPES[0])
141
+ )
142
+ events.push({
143
+ type: TYPES[0],
144
+ summary: `Historical risk check ${current.risk.reviewId}: ${current.risk.state}; risk task count ${current.risk.riskTaskCount ?? "unknown"}. Current risk and goal completion require native verification.`,
145
+ });
146
+ for (const receipt of current.receipts.entries) {
147
+ const before = previous.receipts.entries.find(
148
+ (entry) => entry.id === receipt.id,
149
+ );
150
+ if (
151
+ before &&
152
+ before.status !== "unresolved" &&
153
+ digest(before) !== digest(receipt)
154
+ )
155
+ fail("CLI_GOAL_EVENT_SOURCE_REGRESSED");
156
+ if (
157
+ (!before || digest(receipt) !== digest(before)) &&
158
+ plugin.declaration.events.includes(TYPES[1])
159
+ )
160
+ events.push({
161
+ type: TYPES[1],
162
+ summary: `Historical action receipt ${receipt.id}: ${receipt.status} (${receipt.actionType}). This does not prove goal completion.`,
163
+ });
164
+ }
165
+ for (const candidate of events) {
166
+ const observedAt = new Date().toISOString();
167
+ const event = await _deps.projectGoalExtensionEvent(
168
+ ref,
169
+ { type: candidate.type, goalRef },
170
+ {
171
+ ...admission,
172
+ async authorize(context) {
173
+ if (
174
+ context.operation !== "read-event" ||
175
+ context.type !== candidate.type ||
176
+ JSON.stringify(context.goalRef) !== JSON.stringify(goalRef)
177
+ )
178
+ return false;
179
+ await revalidate(current);
180
+ return true;
181
+ },
182
+ async readEvent() {
183
+ await revalidate(current);
184
+ return { summary: candidate.summary, occurredAt: observedAt };
185
+ },
186
+ },
187
+ );
188
+ await revalidate(current);
189
+ if (controller.signal.aborted || performance.now() >= deadline)
190
+ return;
191
+ yield Object.freeze({
192
+ schema: SCHEMA,
193
+ kind: "event",
194
+ source: "desktop-project",
195
+ plugin: ref,
196
+ observedAt,
197
+ delivery: "polling",
198
+ replay: false,
199
+ event,
200
+ });
201
+ }
202
+ previous = current;
203
+ }
204
+ } catch (error) {
205
+ if (!controller.signal.aborted) throw error;
206
+ } finally {
207
+ clearTimeout(timer);
208
+ signal?.removeEventListener("abort", stop);
209
+ controller.abort();
210
+ }
211
+ })();
212
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "goal-project-observer",
3
+ "version": "0.1.0",
4
+ "description": "Observe bounded native risk-check and action-receipt summaries through a separately authorized desktop event connection",
5
+ "permissions": {
6
+ "goalExtension": [
7
+ "event:risk.review.completed",
8
+ "event:action.receipt.updated"
9
+ ]
10
+ },
11
+ "goalExtension": {
12
+ "apiVersion": "1.0.0",
13
+ "hosts": {
14
+ "cli": ">=0.166.142"
15
+ },
16
+ "events": ["risk.review.completed", "action.receipt.updated"],
17
+ "panels": [],
18
+ "proposals": []
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "goal-status-observer",
3
+ "version": "0.1.0",
4
+ "description": "Observe status changes of one explicitly selected standalone CLI goal",
5
+ "permissions": {
6
+ "goalExtension": ["event:goal.status.changed"]
7
+ },
8
+ "goalExtension": {
9
+ "apiVersion": "1.0.0",
10
+ "hosts": {
11
+ "cli": ">=0.166.142"
12
+ },
13
+ "events": ["goal.status.changed"],
14
+ "panels": [],
15
+ "proposals": []
16
+ }
17
+ }
@@ -0,0 +1,92 @@
1
+ # Project delivery assistant 0.2.0
2
+
3
+ This data-only scenario package combines evidence panels, bounded observations,
4
+ retained event replay and user-selected task-description drafts. It runs through
5
+ the existing CLI and both IDE plugin managers. Requires CLI 0.166.142, Session
6
+ Core 0.3.25 and a matching desktop build with the separately authorized goal
7
+ connections. Paired IDE releases are Open VSX 0.37.178 and JetBrains 0.4.195.
8
+ The version ranges in the manifest are compatibility floors; each operation
9
+ still needs its native source authorization.
10
+
11
+ The package contains no hooks, binaries, arbitrary code or autonomous scheduler.
12
+ It does not create tasks, choose a description for you, approve actions or mark a
13
+ goal complete. Task creation remains a future extension surface.
14
+
15
+ From the repository root, install at the scope you will use:
16
+
17
+ ```powershell
18
+ cc plugin add packages/cli/src/lib/plugin-runtime/examples/project-delivery-assistant --scope project --json
19
+ cc plugin trust project-delivery-assistant --scope project
20
+ cc plugin consent project-delivery-assistant --scope project --grant --json
21
+ ```
22
+
23
+ Consent covers the full declared capability set. Each command still needs its
24
+ own selected source and separate desktop authorization:
25
+
26
+ | Entry | Source and result |
27
+ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `goal panels` | A separately authorized evidence connection; objective/status and historical risk/receipt summary. A summary-only connection cannot supply evidence fields. |
29
+ | `goal watch` with a connection | A v4 event connection; risk-check and action-receipt observations for a bounded interval. Polling can miss intermediate changes. |
30
+ | `goal watch` without a connection | A standalone CLI goal; status changes only. This does not subscribe to native project status. |
31
+ | `goal replay` | A v5 retained-history connection and explicit new/resume checkpoint; one page per invocation. |
32
+ | `goal draft-access`, `draft-sources`, `draft-send`, `draft-receipt` | A separate v3 pending-draft authorization bound to this plugin/version/declaration. The user selects a current native source and a UTF-8 description file. |
33
+
34
+ In the desktop goal area, export the relevant connection with its separate native
35
+ confirmation. Connections are short-lived and tied to the active identity and
36
+ goal revision; keep their private files local. One connection does not grant the
37
+ other surfaces. For example:
38
+
39
+ ```powershell
40
+ cc goal panels <goal-id> --connection "<evidence.json>" --plugin project-delivery-assistant --scope project --json
41
+ cc goal watch <goal-id> --connection "<events-v4.json>" --plugin project-delivery-assistant --scope project --duration 60 --json
42
+ cc goal replay <goal-id> --connection "<history-v5.json>" --checkpoint "<checkpoint.json>" --checkpoint-mode new --plugin project-delivery-assistant --scope project --json
43
+ # Subsequent explicit page: use the same checkpoint and --checkpoint-mode resume.
44
+ cc goal replay <goal-id> --connection "<history-v5.json>" --checkpoint "<checkpoint.json>" --checkpoint-mode resume --plugin project-delivery-assistant --scope project --json
45
+ ```
46
+
47
+ Replay scans at most 20 retained events per page. Exit zero confirms checkpoint
48
+ commit; output alone does not. Keep the checkpoint after interruption because it
49
+ may already have advanced. Delivery can repeat event IDs. The newest 1000
50
+ instrumented events are retained; pre-instrumentation history is not backfilled.
51
+ Retention gaps and database regression are reported, without automatic reset.
52
+
53
+ For a pending description draft, export this plugin's access request first:
54
+
55
+ ```powershell
56
+ cc goal draft-access --plugin project-delivery-assistant --scope project --out "<new-access.json>" --json
57
+ ```
58
+
59
+ In the desktop, choose the separate online-draft authorization and select that
60
+ access file. In either IDE select **Send online draft…** / **Submit pending
61
+ draft…**, pick the v3 connection, a current native source, the proposed description
62
+ and a new submission-record file. Alternatively use the CLI:
63
+
64
+ ```powershell
65
+ cc goal draft-sources <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --json
66
+ cc goal draft-send <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --source "<source-id>" --request-id "<stable-request-id>" --title "Delivery follow-up" --description-file "<description.txt>" --record "<new-submission.json>" --json
67
+ cc goal draft-receipt <goal-id> --connection "<draft-v3.json>" --plugin project-delivery-assistant --scope project --record "<submission.json>" --json
68
+ ```
69
+
70
+ A saved draft is pending. Open it in the desktop, inspect the current native
71
+ preview, then confirm through the existing action flow. Read the real receipt
72
+ and run a fresh risk check to assess the outcome; a description edit does not
73
+ prove that a deadline or dependency risk was resolved. If submission is unknown,
74
+ keep its record and look up the receipt before deciding whether to send again.
75
+
76
+ In VS Code / JetBrains choose this installed plugin, then **CLI goal panels…**,
77
+ **Observe desktop events…**, **Recover retained history…** or the draft controls.
78
+ Recovery uses native connection and new/resume checkpoint dialogs; **Read next
79
+ page / check again** is explicit. No automatic observation, retry or business
80
+ action starts when the package is installed or reinstalled.
81
+
82
+ Disable or uninstall using the normal plugin controls. New operations are refused,
83
+ and active observers stop when they recheck admission. Native goals, actions,
84
+ receipts and saved checkpoints stay in their existing stores. Reinstalling keeps
85
+ the existing trust/consent policy, and requires a new explicit command or IDE
86
+ action to read again; it does not reconnect by itself. Version changes require
87
+ project trust again, capability expansion requires new consent, and a changed
88
+ declaration/goal binding requires a matching new desktop authorization and an
89
+ explicit new checkpoint when the old binding no longer matches.
90
+
91
+ Real IDE/Electron GUI journeys, automatic drafting, task-creation proposals,
92
+ independent business-effect evaluation and release gates remain pending.
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "name": "project-delivery-assistant",
3
- "version": "0.1.0",
4
- "description": "Declarative project delivery assistant prototype; requires a goal extension host adapter",
3
+ "version": "0.2.0",
4
+ "description": "Project evidence, bounded event observation and retained replay, and user-selected description drafts through separately authorized CLI surfaces",
5
5
  "permissions": {
6
6
  "goalExtension": [
7
7
  "event:goal.status.changed",
8
8
  "event:risk.review.completed",
9
9
  "event:action.receipt.updated",
10
10
  "panel:goal.evidence",
11
- "proposal:task.create",
12
11
  "proposal:task.description.update"
13
12
  ]
14
13
  },
15
14
  "goalExtension": {
16
15
  "apiVersion": "1.0.0",
17
16
  "hosts": {
18
- "cli": ">=0.166.104",
19
- "vscode": ">=0.37.149",
20
- "jetbrains": ">=0.4.166"
17
+ "cli": ">=0.166.142",
18
+ "vscode": ">=0.37.178",
19
+ "jetbrains": ">=0.4.195"
21
20
  },
22
21
  "events": [
23
22
  "goal.status.changed",
@@ -33,6 +32,6 @@
33
32
  "fallback": "text"
34
33
  }
35
34
  ],
36
- "proposals": ["task.create", "task.description.update"]
35
+ "proposals": ["task.description.update"]
37
36
  }
38
37
  }
@@ -36,6 +36,11 @@ function fixedHostOptions(options) {
36
36
  id: options.host?.id,
37
37
  version: options.host?.version,
38
38
  capabilities: Object.freeze([...(options.host?.capabilities || [])]),
39
+ activeCapabilities: Object.freeze([
40
+ ...(options.host?.activeCapabilities ??
41
+ options.host?.capabilities ??
42
+ []),
43
+ ]),
39
44
  }),
40
45
  authorize: options.authorize,
41
46
  readEvent: options.readEvent,
@@ -44,6 +49,14 @@ function fixedHostOptions(options) {
44
49
  });
45
50
  }
46
51
 
52
+ function requireActiveCapability(options, capability) {
53
+ if (
54
+ !options.host.capabilities.includes(capability) ||
55
+ !options.host.activeCapabilities.includes(capability)
56
+ )
57
+ throw new Error("goal extension capability is inactive for this operation");
58
+ }
59
+
47
60
  /** Compatibility and mandatory consent are independent of legacy policy flags. */
48
61
  export function collectGoalExtensions(options = {}) {
49
62
  const extensions = [];
@@ -139,6 +152,7 @@ export async function projectGoalExtensionEvent(ref, request, options = {}) {
139
152
  options = fixedHostOptions(options);
140
153
  const type = request?.type;
141
154
  const plugin = admittedPlugin(ref, options, "events", type);
155
+ requireActiveCapability(options, `event:${type}`);
142
156
  const source = goalRef(request.goalRef);
143
157
  const context = Object.freeze({
144
158
  plugin: Object.freeze({
@@ -182,6 +196,7 @@ export async function projectGoalExtensionPanel(ref, request, options = {}) {
182
196
  (entry) => entry.id === request?.panelId,
183
197
  );
184
198
  if (!panel) throw new Error("goal extension panel is undeclared");
199
+ requireActiveCapability(options, `panel:${panel.slot}`);
185
200
  const source = goalRef(request.goalRef);
186
201
  const context = Object.freeze({
187
202
  plugin: Object.freeze({
@@ -229,6 +244,7 @@ export async function proposeGoalExtensionAction(ref, raw, options = {}) {
229
244
  raw = normalizeGoalExtensionProposal(raw);
230
245
  if (!plugin.declaration.proposals.includes(raw.type))
231
246
  throw new Error("goal extension proposal is undeclared");
247
+ requireActiveCapability(options, `proposal:${raw.type}`);
232
248
  const source = raw.goalRef;
233
249
  const title = raw.title;
234
250
  const description = raw.description;
@@ -5,7 +5,11 @@ import http from "node:http";
5
5
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
6
6
  import { inspectPrivatePath } from "@chainlesschain/session-core/private-storage";
7
7
  import { goalExtensionRef } from "@chainlesschain/session-core/goal-extension-proposal-contract";
8
- import { validateGoalEvidenceProjection } from "@chainlesschain/session-core/goal-evidence-contract";
8
+ import {
9
+ validateGoalEvidenceProjection,
10
+ validateGoalEventHistoryPage,
11
+ parseGoalEventCursor,
12
+ } from "@chainlesschain/session-core/goal-evidence-contract";
9
13
  import { validateGoalDraftDescriptor } from "@chainlesschain/session-core/goal-draft-connection-contract";
10
14
  import { createProjectGoalDraftClient } from "./project-goal-draft-connection.js";
11
15
  function fail() {
@@ -59,17 +63,32 @@ export function createProjectGoalConnection(file) {
59
63
  }
60
64
  const evidence =
61
65
  descriptor.schema === "chainlesschain.project-goal-connection/v2";
62
- const endpointPath = evidence ? "/goal-evidence" : "/goal-summary";
66
+ const events =
67
+ descriptor.schema === "chainlesschain.project-goal-connection/v4";
68
+ const history =
69
+ descriptor.schema === "chainlesschain.project-goal-connection/v5";
70
+ const endpointPath = history
71
+ ? "/goal-event-history"
72
+ : events
73
+ ? "/goal-events"
74
+ : evidence
75
+ ? "/goal-evidence"
76
+ : "/goal-summary";
63
77
  if (
64
78
  Object.keys(descriptor).sort().join() !==
65
- (evidence
79
+ (evidence || events || history
66
80
  ? "capability,endpoint,expiresAt,goalRef,key,schema"
67
81
  : "endpoint,expiresAt,goalRef,key,schema") ||
68
- (evidence
69
- ? descriptor.capability !== "goal.evidence"
70
- : descriptor.schema !== "chainlesschain.project-goal-connection/v1") ||
82
+ (history
83
+ ? descriptor.capability !== "goal.event-history"
84
+ : events
85
+ ? descriptor.capability !== "goal.events"
86
+ : evidence
87
+ ? descriptor.capability !== "goal.evidence"
88
+ : descriptor.schema !==
89
+ "chainlesschain.project-goal-connection/v1") ||
71
90
  typeof descriptor.endpoint !== "string" ||
72
- !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/goal-(?:summary|evidence)$/.test(
91
+ !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/goal-(?:summary|evidence|events|event-history)$/.test(
73
92
  descriptor.endpoint,
74
93
  ) ||
75
94
  new URL(descriptor.endpoint).pathname !== endpointPath ||
@@ -93,22 +112,26 @@ export function createProjectGoalConnection(file) {
93
112
  createHmac("sha256", Buffer.from(descriptor.key, "hex"))
94
113
  .update(value)
95
114
  .digest("hex");
96
- async function read(id = ref.id) {
115
+ async function read(id = ref.id, { signal, after } = {}) {
97
116
  validate();
98
117
  if (id !== ref.id) fail();
118
+ if (after !== undefined && !history) fail();
119
+ if (after !== undefined) parseGoalEventCursor(after);
120
+ const requestPath =
121
+ endpointPath + (after === undefined ? "" : `?after=${after}`);
99
122
  const nonce = randomBytes(32).toString("hex");
100
123
  try {
101
124
  const result = await new Promise((resolve, reject) => {
102
125
  const request = http.get(
103
- descriptor.endpoint,
126
+ descriptor.endpoint +
127
+ (after === undefined ? "" : `?after=${after}`),
104
128
  {
105
129
  agent: false,
130
+ signal,
106
131
  maxHeaderSize: 4096,
107
132
  headers: {
108
133
  "X-CC-Nonce": nonce,
109
- "X-CC-Signature": mac(
110
- `request\nGET\n${endpointPath}\n${nonce}`,
111
- ),
134
+ "X-CC-Signature": mac(`request\nGET\n${requestPath}\n${nonce}`),
112
135
  },
113
136
  },
114
137
  (response) => {
@@ -124,19 +147,48 @@ export function createProjectGoalConnection(file) {
124
147
  const body = Buffer.concat(chunks).toString("utf8"),
125
148
  signature = response.headers["x-cc-signature"];
126
149
  if (
127
- response.statusCode !== 200 ||
150
+ (!history && response.statusCode !== 200) ||
128
151
  typeof signature !== "string" ||
129
152
  !/^[a-f0-9]{64}$/.test(signature) ||
130
153
  !timingSafeEqual(
131
154
  Buffer.from(signature),
132
- Buffer.from(mac(`response\n${nonce}\n${body}`)),
155
+ Buffer.from(
156
+ mac(
157
+ history
158
+ ? `response\n${nonce}\n${response.statusCode}\n${body}`
159
+ : `response\n${nonce}\n${body}`,
160
+ ),
161
+ ),
133
162
  )
134
163
  ) {
135
164
  reject(new Error("signature"));
136
165
  return;
137
166
  }
138
167
  try {
139
- resolve(JSON.parse(body));
168
+ const parsed = JSON.parse(body);
169
+ if (history && response.statusCode !== 200) {
170
+ if (
171
+ [409, 410].includes(response.statusCode) &&
172
+ Object.keys(parsed).join() === "error" &&
173
+ [
174
+ "GOAL_EVENT_CURSOR_INVALID",
175
+ "GOAL_EVENT_HISTORY_GAP",
176
+ "GOAL_EVENT_HISTORY_REGRESSED",
177
+ "GOAL_EVENT_HISTORY_UNAVAILABLE",
178
+ "GOAL_EVENT_HISTORY_INVALID",
179
+ ].includes(parsed.error)
180
+ ) {
181
+ reject(
182
+ Object.assign(new Error(parsed.error), {
183
+ code: parsed.error,
184
+ }),
185
+ );
186
+ return;
187
+ }
188
+ reject(new Error("status"));
189
+ return;
190
+ }
191
+ resolve(parsed);
140
192
  } catch {
141
193
  reject(new Error("json"));
142
194
  }
@@ -152,34 +204,66 @@ export function createProjectGoalConnection(file) {
152
204
  request.on("error", reject);
153
205
  });
154
206
  validate();
207
+ if (history) return validateGoalEventHistoryPage(result, ref, after);
155
208
  if (
156
209
  Object.keys(result).sort().join() !==
157
- (evidence
158
- ? "goalRef,objective,receipts,risk,schema,status"
159
- : "goalRef,objective,schema,status") ||
210
+ (events
211
+ ? "goalRef,receipts,risk,schema"
212
+ : evidence
213
+ ? "goalRef,objective,receipts,risk,schema,status"
214
+ : "goalRef,objective,schema,status") ||
160
215
  result.schema !==
161
- (evidence
162
- ? "chainlesschain.project-goal-evidence/v1"
163
- : "chainlesschain.project-goal-summary/v1") ||
216
+ (events
217
+ ? "chainlesschain.project-goal-events/v1"
218
+ : evidence
219
+ ? "chainlesschain.project-goal-evidence/v1"
220
+ : "chainlesschain.project-goal-summary/v1") ||
164
221
  JSON.stringify(goalExtensionRef(result.goalRef)) !==
165
222
  JSON.stringify(ref) ||
166
- typeof result.objective !== "string" ||
167
- result.objective.length > 4096 ||
168
- result.status !== "active"
223
+ (!events &&
224
+ (typeof result.objective !== "string" ||
225
+ result.objective.length > 4096 ||
226
+ result.status !== "active"))
169
227
  )
170
228
  fail();
171
- if (evidence)
229
+ if (evidence || events)
172
230
  validateGoalEvidenceProjection({
173
231
  risk: result.risk,
174
232
  receipts: result.receipts,
175
233
  });
176
234
  return { ...result, goalRef: ref };
177
- } catch {
235
+ } catch (error) {
236
+ if (
237
+ history &&
238
+ [
239
+ "GOAL_EVENT_CURSOR_INVALID",
240
+ "GOAL_EVENT_HISTORY_GAP",
241
+ "GOAL_EVENT_HISTORY_REGRESSED",
242
+ "GOAL_EVENT_HISTORY_UNAVAILABLE",
243
+ "GOAL_EVENT_HISTORY_INVALID",
244
+ ].includes(error.code)
245
+ )
246
+ throw error;
178
247
  fail();
179
248
  }
180
249
  }
181
250
  validate();
182
- return Object.freeze({ goalRef: ref, evidence, read });
251
+ return history
252
+ ? Object.freeze({
253
+ goalRef: ref,
254
+ eventHistory: true,
255
+ readEventPage: (after, options = {}) =>
256
+ read(ref.id, { ...options, after }),
257
+ read: async () => fail(),
258
+ })
259
+ : events
260
+ ? Object.freeze({
261
+ goalRef: ref,
262
+ events: true,
263
+ readEvents: read,
264
+ read: async () => fail(),
265
+ })
266
+ : Object.freeze({ goalRef: ref, evidence, read });
183
267
  } catch {
184
268
  fail();
185
269
  } finally {