@gmickel/gno 1.23.0 → 1.25.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 +26 -12
- package/assets/skill/SKILL.md +37 -19
- package/assets/skill/recipes/capture-and-file.md +20 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +1 -0
- package/browser-extension/dist/PRIVACY.md +55 -0
- package/browser-extension/dist/chunk-vn5f663b.js +50 -0
- package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
- package/browser-extension/dist/content.js +1 -0
- package/browser-extension/dist/manifest.json +25 -0
- package/browser-extension/dist/preview.html +13 -0
- package/browser-extension/dist/service-worker.js +40 -0
- package/package.json +13 -3
- package/spec/cli.md +141 -0
- package/spec/db/schema.sql +101 -0
- package/spec/mcp.md +10 -0
- package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
- package/spec/output-schemas/browser-clip.schema.json +586 -0
- package/spec/output-schemas/capture-receipt.schema.json +22 -1
- package/spec/output-schemas/clipper-csrf.schema.json +12 -0
- package/spec/output-schemas/clipper-error.schema.json +46 -0
- package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
- package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
- package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
- package/spec/output-schemas/clipper-revoke.schema.json +28 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
- package/spec/output-schemas/setup-activation-result.schema.json +456 -0
- package/spec/output-schemas/setup-command-result.schema.json +93 -0
- package/spec/output-schemas/setup-receipt.schema.json +258 -0
- package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/embed.ts +7 -2
- package/src/cli/commands/setup-activation.ts +324 -0
- package/src/cli/commands/setup-semantic.ts +591 -0
- package/src/cli/commands/setup.ts +410 -0
- package/src/cli/program.ts +64 -0
- package/src/cli/setup-semantic-worker.ts +177 -0
- package/src/core/browser-clip-provenance.ts +139 -0
- package/src/core/browser-clip.ts +473 -0
- package/src/core/capture-write.ts +5 -0
- package/src/core/capture.ts +75 -18
- package/src/core/config-mutation.ts +94 -64
- package/src/core/file-lock.ts +89 -36
- package/src/core/folder-setup-planning.ts +453 -0
- package/src/core/folder-setup.ts +490 -0
- package/src/core/setup-activation.ts +309 -0
- package/src/core/setup-receipt.ts +321 -0
- package/src/serve/capture-service.ts +420 -0
- package/src/serve/clipper-body.ts +62 -0
- package/src/serve/clipper-capture.ts +248 -0
- package/src/serve/clipper-contract.ts +57 -0
- package/src/serve/clipper-idempotency.ts +35 -0
- package/src/serve/clipper-pairing.ts +297 -0
- package/src/serve/clipper-security-errors.ts +23 -0
- package/src/serve/clipper-security.ts +449 -0
- package/src/serve/connectors.ts +29 -2
- package/src/serve/public/app.tsx +8 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/index.html +1 -0
- package/src/serve/public/lib/clipper-approval.ts +206 -0
- package/src/serve/public/pages/ClipperPairing.tsx +210 -0
- package/src/serve/routes/api.ts +19 -115
- package/src/serve/routes/clipper.ts +394 -0
- package/src/serve/server.ts +22 -0
- package/src/store/migrations/020-browser-clipper-security.ts +128 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/clipper-store-types.ts +104 -0
- package/src/store/sqlite/clipper-store.ts +496 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationVerificationCode,
|
|
3
|
+
ActivationVerificationReceipt,
|
|
4
|
+
StorePort,
|
|
5
|
+
StoreResult,
|
|
6
|
+
} from "../store/types";
|
|
7
|
+
import type { ConnectorVerificationCode } from "./connector-policy";
|
|
8
|
+
|
|
9
|
+
import { getConnectorVerificationRemediation } from "./connector-policy";
|
|
10
|
+
|
|
11
|
+
export const SETUP_ACTIVATION_SCHEMA_VERSION = "1.0" as const;
|
|
12
|
+
|
|
13
|
+
export type SetupConnectorInstallation = "installed" | "reused" | "failed";
|
|
14
|
+
export type SetupConnectorVerification =
|
|
15
|
+
| "passed"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "skipped"
|
|
18
|
+
| "not_run";
|
|
19
|
+
export type SetupConnectorCode =
|
|
20
|
+
| "connector_verified"
|
|
21
|
+
| "connector_configuration_unavailable"
|
|
22
|
+
| "connector_install_failed"
|
|
23
|
+
| "connector_verification_failed"
|
|
24
|
+
| ConnectorVerificationCode;
|
|
25
|
+
|
|
26
|
+
export interface SetupConnectorDefinition {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: "skill" | "mcp";
|
|
29
|
+
target: string;
|
|
30
|
+
scope: "user" | "project";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SetupConnectorState extends SetupConnectorDefinition {
|
|
34
|
+
installed: boolean;
|
|
35
|
+
configurationError: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SetupConnectorResult {
|
|
39
|
+
connectorId: string;
|
|
40
|
+
kind: "skill" | "mcp";
|
|
41
|
+
target: string;
|
|
42
|
+
scope: "user" | "project";
|
|
43
|
+
installation: SetupConnectorInstallation;
|
|
44
|
+
verification: SetupConnectorVerification;
|
|
45
|
+
code: SetupConnectorCode;
|
|
46
|
+
remediation: string;
|
|
47
|
+
receipt: ActivationVerificationReceipt | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SetupConnectorComposition {
|
|
51
|
+
status: "completed" | "completed_with_actions";
|
|
52
|
+
connectors: SetupConnectorResult[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SetupConnectorInstallContext {
|
|
56
|
+
indexName: string;
|
|
57
|
+
configPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SetupConnectorCompositionDeps {
|
|
61
|
+
getStates: () => Promise<SetupConnectorState[]>;
|
|
62
|
+
install: (
|
|
63
|
+
connectorId: string,
|
|
64
|
+
context: SetupConnectorInstallContext
|
|
65
|
+
) => Promise<SetupConnectorState>;
|
|
66
|
+
verify: (
|
|
67
|
+
connectorId: string,
|
|
68
|
+
store: StorePort,
|
|
69
|
+
collection: string
|
|
70
|
+
) => Promise<StoreResult<ActivationVerificationReceipt>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ComposeSetupConnectorsOptions {
|
|
74
|
+
connectorIds: string[];
|
|
75
|
+
definitions: SetupConnectorDefinition[];
|
|
76
|
+
collection: string;
|
|
77
|
+
store: StorePort;
|
|
78
|
+
installContext: SetupConnectorInstallContext;
|
|
79
|
+
deps: SetupConnectorCompositionDeps;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const completedRemediation = "No action required.";
|
|
83
|
+
const installFailureRemediation =
|
|
84
|
+
"Repair the selected connector configuration or permissions, then rerun the same setup command.";
|
|
85
|
+
const configurationFailureRemediation =
|
|
86
|
+
"Repair the selected connector configuration without overwriting it, then rerun the same setup command.";
|
|
87
|
+
const verificationFailureRemediation =
|
|
88
|
+
"Retry the same setup command; if verification still fails, run gno doctor.";
|
|
89
|
+
|
|
90
|
+
function isConnectorVerificationCode(
|
|
91
|
+
code: ActivationVerificationCode | undefined
|
|
92
|
+
): code is ConnectorVerificationCode {
|
|
93
|
+
return (
|
|
94
|
+
code === "connector_not_configured" ||
|
|
95
|
+
code === "connector_probe_unavailable" ||
|
|
96
|
+
code === "connector_unsupported_config" ||
|
|
97
|
+
code === "connector_start_failed" ||
|
|
98
|
+
code === "connector_timeout" ||
|
|
99
|
+
code === "connector_missing_tools" ||
|
|
100
|
+
code === "connector_status_failed" ||
|
|
101
|
+
code === "connector_search_failed" ||
|
|
102
|
+
code === "connector_result_mismatch" ||
|
|
103
|
+
code === "target_runtime_unverifiable"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dedupeConnectorIds(connectorIds: string[]): string[] {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
return connectorIds.filter((connectorId) => {
|
|
110
|
+
if (seen.has(connectorId)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
seen.add(connectorId);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function failedResult(
|
|
119
|
+
definition: SetupConnectorDefinition,
|
|
120
|
+
input: {
|
|
121
|
+
installation: SetupConnectorInstallation;
|
|
122
|
+
code:
|
|
123
|
+
| "connector_configuration_unavailable"
|
|
124
|
+
| "connector_install_failed"
|
|
125
|
+
| "connector_verification_failed";
|
|
126
|
+
remediation: string;
|
|
127
|
+
}
|
|
128
|
+
): SetupConnectorResult {
|
|
129
|
+
return {
|
|
130
|
+
connectorId: definition.id,
|
|
131
|
+
kind: definition.kind,
|
|
132
|
+
target: definition.target,
|
|
133
|
+
scope: definition.scope,
|
|
134
|
+
installation: input.installation,
|
|
135
|
+
verification:
|
|
136
|
+
input.code === "connector_verification_failed" ? "failed" : "not_run",
|
|
137
|
+
code: input.code,
|
|
138
|
+
remediation: input.remediation,
|
|
139
|
+
receipt: null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function receiptResult(
|
|
144
|
+
definition: SetupConnectorDefinition,
|
|
145
|
+
installation: SetupConnectorInstallation,
|
|
146
|
+
receipt: ActivationVerificationReceipt
|
|
147
|
+
): SetupConnectorResult {
|
|
148
|
+
const connectorStage = receipt.stages.connector;
|
|
149
|
+
const verification =
|
|
150
|
+
connectorStage.status === "passed" ||
|
|
151
|
+
connectorStage.status === "failed" ||
|
|
152
|
+
connectorStage.status === "skipped"
|
|
153
|
+
? connectorStage.status
|
|
154
|
+
: "failed";
|
|
155
|
+
const code =
|
|
156
|
+
connectorStage.status === "passed"
|
|
157
|
+
? "connector_verified"
|
|
158
|
+
: isConnectorVerificationCode(connectorStage.code)
|
|
159
|
+
? connectorStage.code
|
|
160
|
+
: "connector_verification_failed";
|
|
161
|
+
const remediation =
|
|
162
|
+
code === "connector_verified"
|
|
163
|
+
? completedRemediation
|
|
164
|
+
: code === "connector_verification_failed"
|
|
165
|
+
? verificationFailureRemediation
|
|
166
|
+
: getConnectorVerificationRemediation(code, definition.target);
|
|
167
|
+
return {
|
|
168
|
+
connectorId: definition.id,
|
|
169
|
+
kind: definition.kind,
|
|
170
|
+
target: definition.target,
|
|
171
|
+
scope: definition.scope,
|
|
172
|
+
installation,
|
|
173
|
+
verification,
|
|
174
|
+
code,
|
|
175
|
+
remediation,
|
|
176
|
+
receipt,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function unavailableSetupConnectorComposition(
|
|
181
|
+
definitions: SetupConnectorDefinition[]
|
|
182
|
+
): SetupConnectorComposition {
|
|
183
|
+
return {
|
|
184
|
+
status: "completed_with_actions",
|
|
185
|
+
connectors: definitions.map((definition) => ({
|
|
186
|
+
connectorId: definition.id,
|
|
187
|
+
kind: definition.kind,
|
|
188
|
+
target: definition.target,
|
|
189
|
+
scope: definition.scope,
|
|
190
|
+
installation: "failed",
|
|
191
|
+
verification: "not_run",
|
|
192
|
+
code: "connector_verification_failed",
|
|
193
|
+
remediation: verificationFailureRemediation,
|
|
194
|
+
receipt: null,
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function currentStateAfterInstallFailure(
|
|
200
|
+
connectorId: string,
|
|
201
|
+
deps: SetupConnectorCompositionDeps
|
|
202
|
+
): Promise<SetupConnectorState | null> {
|
|
203
|
+
try {
|
|
204
|
+
const states = await deps.getStates();
|
|
205
|
+
return states.find(({ id }) => id === connectorId) ?? null;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function composeOneConnector(
|
|
212
|
+
definition: SetupConnectorDefinition,
|
|
213
|
+
initialState: SetupConnectorState | null,
|
|
214
|
+
options: ComposeSetupConnectorsOptions
|
|
215
|
+
): Promise<SetupConnectorResult> {
|
|
216
|
+
if (!initialState || initialState.configurationError) {
|
|
217
|
+
return failedResult(definition, {
|
|
218
|
+
installation: "failed",
|
|
219
|
+
code: "connector_configuration_unavailable",
|
|
220
|
+
remediation: configurationFailureRemediation,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let installation: SetupConnectorInstallation = "reused";
|
|
225
|
+
if (!initialState.installed) {
|
|
226
|
+
try {
|
|
227
|
+
const installed = await options.deps.install(
|
|
228
|
+
definition.id,
|
|
229
|
+
options.installContext
|
|
230
|
+
);
|
|
231
|
+
if (!installed.installed || installed.configurationError) {
|
|
232
|
+
return failedResult(definition, {
|
|
233
|
+
installation: "failed",
|
|
234
|
+
code: "connector_install_failed",
|
|
235
|
+
remediation: installFailureRemediation,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
installation = "installed";
|
|
239
|
+
} catch {
|
|
240
|
+
const racedState = await currentStateAfterInstallFailure(
|
|
241
|
+
definition.id,
|
|
242
|
+
options.deps
|
|
243
|
+
);
|
|
244
|
+
if (!racedState?.installed || racedState.configurationError) {
|
|
245
|
+
return failedResult(definition, {
|
|
246
|
+
installation: "failed",
|
|
247
|
+
code: "connector_install_failed",
|
|
248
|
+
remediation: installFailureRemediation,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
installation = "reused";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const verification = await options.deps.verify(
|
|
257
|
+
definition.id,
|
|
258
|
+
options.store,
|
|
259
|
+
options.collection
|
|
260
|
+
);
|
|
261
|
+
if (!verification.ok) {
|
|
262
|
+
return failedResult(definition, {
|
|
263
|
+
installation,
|
|
264
|
+
code: "connector_verification_failed",
|
|
265
|
+
remediation: verificationFailureRemediation,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return receiptResult(definition, installation, verification.value);
|
|
269
|
+
} catch {
|
|
270
|
+
return failedResult(definition, {
|
|
271
|
+
installation,
|
|
272
|
+
code: "connector_verification_failed",
|
|
273
|
+
remediation: verificationFailureRemediation,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Compose requested connector install/verification after lexical setup.
|
|
280
|
+
* Callers own store lifecycle and validation of connector IDs.
|
|
281
|
+
*/
|
|
282
|
+
export async function composeSetupConnectors(
|
|
283
|
+
options: ComposeSetupConnectorsOptions
|
|
284
|
+
): Promise<SetupConnectorComposition> {
|
|
285
|
+
const requestedIds = dedupeConnectorIds(options.connectorIds);
|
|
286
|
+
let states: SetupConnectorState[] = [];
|
|
287
|
+
try {
|
|
288
|
+
states = await options.deps.getStates();
|
|
289
|
+
} catch {
|
|
290
|
+
// Per-target bounded results below deliberately hide the unbounded error.
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const results: SetupConnectorResult[] = [];
|
|
294
|
+
for (const connectorId of requestedIds) {
|
|
295
|
+
const definition = options.definitions.find(({ id }) => id === connectorId);
|
|
296
|
+
if (!definition) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const state = states.find(({ id }) => id === connectorId) ?? null;
|
|
300
|
+
results.push(await composeOneConnector(definition, state, options));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
status: results.every(({ verification }) => verification === "passed")
|
|
305
|
+
? "completed"
|
|
306
|
+
: "completed_with_actions",
|
|
307
|
+
connectors: results,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical, privacy-bounded receipts for resumable folder setup.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/setup-receipt
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises provides private file creation, directory permissions, and atomic rename APIs that Bun does not expose.
|
|
8
|
+
import { chmod, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
9
|
+
// node:path has no Bun equivalent.
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { ActivationVerificationReceipt } from "../store/types";
|
|
13
|
+
|
|
14
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
15
|
+
|
|
16
|
+
export const SETUP_RECEIPT_SCHEMA_VERSION = "1.0" as const;
|
|
17
|
+
|
|
18
|
+
export const SETUP_STAGE_NAMES = [
|
|
19
|
+
"preflight",
|
|
20
|
+
"config_saved",
|
|
21
|
+
"store_synced",
|
|
22
|
+
"lexical_indexed",
|
|
23
|
+
"lexical_proved",
|
|
24
|
+
"completed",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type SetupStageName = (typeof SETUP_STAGE_NAMES)[number];
|
|
28
|
+
export type SetupStageStatus = "pending" | "in_progress" | "passed" | "failed";
|
|
29
|
+
|
|
30
|
+
export interface SetupStageReceipt {
|
|
31
|
+
status: SetupStageStatus;
|
|
32
|
+
token: string | null;
|
|
33
|
+
startedAt: string | null;
|
|
34
|
+
completedAt: string | null;
|
|
35
|
+
code: string | null;
|
|
36
|
+
remediation: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SetupFailure {
|
|
40
|
+
stage: SetupStageName;
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
remediation: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FolderSetupReceipt {
|
|
47
|
+
schemaVersion: typeof SETUP_RECEIPT_SCHEMA_VERSION;
|
|
48
|
+
status: "in_progress" | "failed" | "completed";
|
|
49
|
+
generatedAt: string;
|
|
50
|
+
input: {
|
|
51
|
+
folder: string;
|
|
52
|
+
folderFingerprint: string;
|
|
53
|
+
indexName: string;
|
|
54
|
+
requestedName: string | null;
|
|
55
|
+
excludes: string[];
|
|
56
|
+
secretRiskAuthorized: boolean;
|
|
57
|
+
};
|
|
58
|
+
fingerprints: {
|
|
59
|
+
input: string;
|
|
60
|
+
config: string | null;
|
|
61
|
+
index: string | null;
|
|
62
|
+
};
|
|
63
|
+
collection: {
|
|
64
|
+
name: string | null;
|
|
65
|
+
path: string;
|
|
66
|
+
disposition: "pending" | "created" | "reused";
|
|
67
|
+
};
|
|
68
|
+
paths: {
|
|
69
|
+
config: string;
|
|
70
|
+
receipt: string;
|
|
71
|
+
};
|
|
72
|
+
stages: Record<SetupStageName, SetupStageReceipt>;
|
|
73
|
+
pending: string[];
|
|
74
|
+
failure: SetupFailure | null;
|
|
75
|
+
activation: ActivationVerificationReceipt | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type CanonicalJson =
|
|
79
|
+
| boolean
|
|
80
|
+
| null
|
|
81
|
+
| number
|
|
82
|
+
| string
|
|
83
|
+
| CanonicalJson[]
|
|
84
|
+
| { [key: string]: CanonicalJson };
|
|
85
|
+
|
|
86
|
+
function canonicalize(value: unknown): CanonicalJson {
|
|
87
|
+
if (
|
|
88
|
+
value === null ||
|
|
89
|
+
typeof value === "boolean" ||
|
|
90
|
+
typeof value === "number" ||
|
|
91
|
+
typeof value === "string"
|
|
92
|
+
) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map((item) => canonicalize(item));
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "object") {
|
|
99
|
+
const output: Record<string, CanonicalJson> = {};
|
|
100
|
+
for (const key of Object.keys(value).sort()) {
|
|
101
|
+
const item = (value as Record<string, unknown>)[key];
|
|
102
|
+
if (item !== undefined) {
|
|
103
|
+
output[key] = canonicalize(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
throw new TypeError(`Unsupported canonical JSON value: ${typeof value}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function serializeSetupReceipt(receipt: FolderSetupReceipt): string {
|
|
112
|
+
return `${JSON.stringify(canonicalize(receipt), null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function setupFingerprint(value: unknown): string {
|
|
116
|
+
return new Bun.CryptoHasher("sha256")
|
|
117
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
118
|
+
.digest("hex");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function setupRootFingerprint(folderRealpath: string): string {
|
|
122
|
+
return new Bun.CryptoHasher("sha256").update(folderRealpath).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getSetupReceiptPath(input: {
|
|
126
|
+
dataDir: string;
|
|
127
|
+
indexName: string;
|
|
128
|
+
folderRealpath: string;
|
|
129
|
+
}): string {
|
|
130
|
+
const indexIdentity = canonicalizeIndexName(input.indexName);
|
|
131
|
+
const rootFingerprint = setupRootFingerprint(input.folderRealpath);
|
|
132
|
+
return join(
|
|
133
|
+
input.dataDir,
|
|
134
|
+
"setup-receipts",
|
|
135
|
+
indexIdentity,
|
|
136
|
+
`${rootFingerprint}.json`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createSetupReceipt(input: {
|
|
141
|
+
now: string;
|
|
142
|
+
folder: string;
|
|
143
|
+
indexName: string;
|
|
144
|
+
requestedName?: string;
|
|
145
|
+
excludes: string[];
|
|
146
|
+
secretRiskAuthorized: boolean;
|
|
147
|
+
configPath: string;
|
|
148
|
+
dataDir: string;
|
|
149
|
+
}): FolderSetupReceipt {
|
|
150
|
+
const indexName = canonicalizeIndexName(input.indexName);
|
|
151
|
+
const folderFingerprint = setupRootFingerprint(input.folder);
|
|
152
|
+
const receiptPath = getSetupReceiptPath({
|
|
153
|
+
dataDir: input.dataDir,
|
|
154
|
+
indexName,
|
|
155
|
+
folderRealpath: input.folder,
|
|
156
|
+
});
|
|
157
|
+
const stages = Object.fromEntries(
|
|
158
|
+
SETUP_STAGE_NAMES.map((stage) => [
|
|
159
|
+
stage,
|
|
160
|
+
{
|
|
161
|
+
status: "pending",
|
|
162
|
+
token: null,
|
|
163
|
+
startedAt: null,
|
|
164
|
+
completedAt: null,
|
|
165
|
+
code: null,
|
|
166
|
+
remediation: null,
|
|
167
|
+
},
|
|
168
|
+
])
|
|
169
|
+
) as Record<SetupStageName, SetupStageReceipt>;
|
|
170
|
+
const requestedName = input.requestedName?.trim().toLowerCase() || null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
schemaVersion: SETUP_RECEIPT_SCHEMA_VERSION,
|
|
174
|
+
status: "in_progress",
|
|
175
|
+
generatedAt: input.now,
|
|
176
|
+
input: {
|
|
177
|
+
folder: input.folder,
|
|
178
|
+
folderFingerprint,
|
|
179
|
+
indexName,
|
|
180
|
+
requestedName,
|
|
181
|
+
excludes: [...new Set(input.excludes)].sort(),
|
|
182
|
+
secretRiskAuthorized: input.secretRiskAuthorized,
|
|
183
|
+
},
|
|
184
|
+
fingerprints: {
|
|
185
|
+
input: setupFingerprint({
|
|
186
|
+
folder: input.folder,
|
|
187
|
+
indexName,
|
|
188
|
+
requestedName,
|
|
189
|
+
excludes: [...new Set(input.excludes)].sort(),
|
|
190
|
+
secretRiskAuthorized: input.secretRiskAuthorized,
|
|
191
|
+
}),
|
|
192
|
+
config: null,
|
|
193
|
+
index: null,
|
|
194
|
+
},
|
|
195
|
+
collection: {
|
|
196
|
+
name: null,
|
|
197
|
+
path: input.folder,
|
|
198
|
+
disposition: "pending",
|
|
199
|
+
},
|
|
200
|
+
paths: {
|
|
201
|
+
config: input.configPath,
|
|
202
|
+
receipt: receiptPath,
|
|
203
|
+
},
|
|
204
|
+
stages,
|
|
205
|
+
pending: [],
|
|
206
|
+
failure: null,
|
|
207
|
+
activation: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function startSetupStage(
|
|
212
|
+
receipt: FolderSetupReceipt,
|
|
213
|
+
stage: SetupStageName,
|
|
214
|
+
now: string
|
|
215
|
+
): void {
|
|
216
|
+
receipt.status = "in_progress";
|
|
217
|
+
receipt.generatedAt = now;
|
|
218
|
+
receipt.failure = null;
|
|
219
|
+
receipt.stages[stage] = {
|
|
220
|
+
status: "in_progress",
|
|
221
|
+
token: setupFingerprint({
|
|
222
|
+
receipt: receipt.input.folderFingerprint,
|
|
223
|
+
stage,
|
|
224
|
+
startedAt: now,
|
|
225
|
+
}),
|
|
226
|
+
startedAt: now,
|
|
227
|
+
completedAt: null,
|
|
228
|
+
code: null,
|
|
229
|
+
remediation: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function passSetupStage(
|
|
234
|
+
receipt: FolderSetupReceipt,
|
|
235
|
+
stage: SetupStageName,
|
|
236
|
+
now: string
|
|
237
|
+
): void {
|
|
238
|
+
receipt.generatedAt = now;
|
|
239
|
+
receipt.stages[stage] = {
|
|
240
|
+
...receipt.stages[stage],
|
|
241
|
+
status: "passed",
|
|
242
|
+
completedAt: now,
|
|
243
|
+
code: null,
|
|
244
|
+
remediation: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function failSetupStage(
|
|
249
|
+
receipt: FolderSetupReceipt,
|
|
250
|
+
failure: SetupFailure,
|
|
251
|
+
now: string
|
|
252
|
+
): void {
|
|
253
|
+
const current = receipt.stages[failure.stage];
|
|
254
|
+
receipt.status = "failed";
|
|
255
|
+
receipt.generatedAt = now;
|
|
256
|
+
receipt.failure = failure;
|
|
257
|
+
if (current.status !== "passed") {
|
|
258
|
+
receipt.stages[failure.stage] = {
|
|
259
|
+
...current,
|
|
260
|
+
status: "failed",
|
|
261
|
+
token:
|
|
262
|
+
current.token ??
|
|
263
|
+
setupFingerprint({
|
|
264
|
+
receipt: receipt.input.folderFingerprint,
|
|
265
|
+
stage: failure.stage,
|
|
266
|
+
startedAt: now,
|
|
267
|
+
}),
|
|
268
|
+
startedAt: current.startedAt ?? now,
|
|
269
|
+
completedAt: now,
|
|
270
|
+
code: failure.code,
|
|
271
|
+
remediation: failure.remediation,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function persistSetupReceipt(
|
|
277
|
+
receipt: FolderSetupReceipt
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const receiptDir = dirname(receipt.paths.receipt);
|
|
280
|
+
await mkdir(receiptDir, { recursive: true, mode: 0o700 });
|
|
281
|
+
await chmod(receiptDir, 0o700);
|
|
282
|
+
|
|
283
|
+
const tempPath = `${receipt.paths.receipt}.tmp.${crypto.randomUUID()}`;
|
|
284
|
+
let tempFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
285
|
+
try {
|
|
286
|
+
tempFile = await open(tempPath, "wx", 0o600);
|
|
287
|
+
await tempFile.writeFile(serializeSetupReceipt(receipt), "utf8");
|
|
288
|
+
await tempFile.sync();
|
|
289
|
+
await tempFile.close();
|
|
290
|
+
tempFile = null;
|
|
291
|
+
await rename(tempPath, receipt.paths.receipt);
|
|
292
|
+
await chmod(receipt.paths.receipt, 0o600);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
await tempFile?.close().catch(() => {
|
|
295
|
+
/* best-effort temporary receipt handle cleanup */
|
|
296
|
+
});
|
|
297
|
+
await unlink(tempPath).catch(() => {
|
|
298
|
+
/* best-effort temporary receipt cleanup */
|
|
299
|
+
});
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function loadSetupReceipt(
|
|
305
|
+
path: string
|
|
306
|
+
): Promise<FolderSetupReceipt | null> {
|
|
307
|
+
const file = Bun.file(path);
|
|
308
|
+
if (!(await file.exists())) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const value: unknown = await file.json();
|
|
312
|
+
if (
|
|
313
|
+
typeof value !== "object" ||
|
|
314
|
+
value === null ||
|
|
315
|
+
!("schemaVersion" in value) ||
|
|
316
|
+
value.schemaVersion !== SETUP_RECEIPT_SCHEMA_VERSION
|
|
317
|
+
) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return value as FolderSetupReceipt;
|
|
321
|
+
}
|