@everfur/cli 0.1.0 → 0.2.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 +295 -19
- package/dist/{chunk-67H43MI6.js → chunk-4UB26D67.js} +295 -510
- package/dist/chunk-FC23PLWG.js +161 -0
- package/dist/chunk-KQICUK54.js +12852 -0
- package/dist/chunk-SJUEHHU6.js +893 -0
- package/dist/chunk-XMIERMOW.js +2512 -0
- package/dist/everfur-mcp-container.d.ts +2 -0
- package/dist/everfur-mcp-container.js +60 -0
- package/dist/everfur-mcp-hosted.d.ts +2 -0
- package/dist/everfur-mcp-hosted.js +28 -0
- package/dist/everfur-mcp-http.d.ts +2 -0
- package/dist/everfur-mcp-http.js +21 -0
- package/dist/everfur-mcp.js +11 -365
- package/dist/everfur.js +4 -3
- package/dist/index.d.ts +875 -41
- package/dist/index.js +2 -21
- package/package.json +5 -4
- package/schema/diagnosis.v1.schema.json +41 -0
- package/schema/plan.v1.schema.json +90 -4
- package/schema/verification.v1.schema.json +7 -3
- package/contract/contract-public.json +0 -1335
- package/dist/chunk-KQZ6ZXL7.js +0 -3994
package/dist/chunk-KQZ6ZXL7.js
DELETED
|
@@ -1,3994 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { statSync, readFileSync } from 'fs';
|
|
3
|
-
import { isAbsolute, resolve, join } from 'path';
|
|
4
|
-
import { parse } from 'yaml';
|
|
5
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
-
import addFormats from 'ajv-formats';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
|
|
9
|
-
var nodeFileSystem = Object.freeze({
|
|
10
|
-
readFile(path) {
|
|
11
|
-
try {
|
|
12
|
-
const stat = statSync(path);
|
|
13
|
-
if (!stat.isFile()) return null;
|
|
14
|
-
return readFileSync(path, "utf8");
|
|
15
|
-
} catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
exists(path) {
|
|
20
|
-
try {
|
|
21
|
-
statSync(path);
|
|
22
|
-
return true;
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
isDirectory(path) {
|
|
28
|
-
try {
|
|
29
|
-
return statSync(path).isDirectory();
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// src/http/transport.ts
|
|
37
|
-
var IDEMPOTENT_METHODS = Object.freeze(["GET", "DELETE"]);
|
|
38
|
-
var TransportError = class extends Error {
|
|
39
|
-
kind;
|
|
40
|
-
constructor(kind, message) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "TransportError";
|
|
43
|
-
this.kind = kind;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
function isIdempotent(method) {
|
|
47
|
-
return IDEMPOTENT_METHODS.includes(method);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// src/http/fetch-transport.ts
|
|
51
|
-
var fetchTransport = Object.freeze({
|
|
52
|
-
async send(request) {
|
|
53
|
-
let response;
|
|
54
|
-
try {
|
|
55
|
-
response = await fetch(request.url, {
|
|
56
|
-
method: request.method,
|
|
57
|
-
headers: { ...request.headers },
|
|
58
|
-
body: request.body,
|
|
59
|
-
redirect: "error",
|
|
60
|
-
signal: AbortSignal.timeout(request.timeoutMs)
|
|
61
|
-
});
|
|
62
|
-
} catch (cause) {
|
|
63
|
-
const name = cause instanceof Error ? cause.name : "";
|
|
64
|
-
if (name === "TimeoutError" || name === "AbortError") {
|
|
65
|
-
throw new TransportError("timeout", `no response within ${request.timeoutMs}ms`);
|
|
66
|
-
}
|
|
67
|
-
throw new TransportError("network", cause instanceof Error ? cause.message : String(cause));
|
|
68
|
-
}
|
|
69
|
-
let body;
|
|
70
|
-
try {
|
|
71
|
-
body = await response.text();
|
|
72
|
-
} catch (cause) {
|
|
73
|
-
throw new TransportError("network", cause instanceof Error ? cause.message : String(cause));
|
|
74
|
-
}
|
|
75
|
-
const headers = {};
|
|
76
|
-
response.headers.forEach((value, key) => {
|
|
77
|
-
headers[key.toLowerCase()] = value;
|
|
78
|
-
});
|
|
79
|
-
return Object.freeze({ status: response.status, headers: Object.freeze(headers), body });
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// src/time/clock.ts
|
|
84
|
-
var systemClock = Object.freeze({
|
|
85
|
-
sleep(milliseconds) {
|
|
86
|
-
return new Promise((resolve2) => {
|
|
87
|
-
setTimeout(resolve2, milliseconds);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
function createRecordingClock() {
|
|
92
|
-
const waits = [];
|
|
93
|
-
return {
|
|
94
|
-
waits,
|
|
95
|
-
sleep(milliseconds) {
|
|
96
|
-
waits.push(milliseconds);
|
|
97
|
-
return Promise.resolve();
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// src/config/endpoint.ts
|
|
103
|
-
var BASE_URL_ENV_VAR = "EVERFUR_API_BASE_URL";
|
|
104
|
-
var API_PREFIX = "/api/v1";
|
|
105
|
-
function stripTrailingSlashes(value) {
|
|
106
|
-
let end = value.length;
|
|
107
|
-
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
108
|
-
return value.slice(0, end);
|
|
109
|
-
}
|
|
110
|
-
function resolveEndpoint(flagValue, env) {
|
|
111
|
-
const fromEnv = (env[BASE_URL_ENV_VAR] ?? "").trim();
|
|
112
|
-
const raw = flagValue !== null ? flagValue.trim() : fromEnv;
|
|
113
|
-
const source = flagValue !== null ? "flag" : "env";
|
|
114
|
-
if (raw.length === 0) {
|
|
115
|
-
return {
|
|
116
|
-
ok: false,
|
|
117
|
-
errors: [
|
|
118
|
-
"no API base URL was configured",
|
|
119
|
-
`pass --base-url <url> or export ${BASE_URL_ENV_VAR}`
|
|
120
|
-
]
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
let parsed;
|
|
124
|
-
try {
|
|
125
|
-
parsed = new URL(raw);
|
|
126
|
-
} catch {
|
|
127
|
-
return { ok: false, errors: [`--base-url is not a URL: ${raw}`] };
|
|
128
|
-
}
|
|
129
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
130
|
-
return { ok: false, errors: [`--base-url must be http or https, not ${parsed.protocol}`] };
|
|
131
|
-
}
|
|
132
|
-
const normalised = stripTrailingSlashes(`${parsed.origin}${parsed.pathname}`);
|
|
133
|
-
return {
|
|
134
|
-
ok: true,
|
|
135
|
-
endpoint: Object.freeze({
|
|
136
|
-
baseUrl: normalised,
|
|
137
|
-
prefixIncluded: normalised.endsWith(API_PREFIX),
|
|
138
|
-
source
|
|
139
|
-
})
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function apiUrl(endpoint, path) {
|
|
143
|
-
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
144
|
-
return endpoint.prefixIncluded ? `${endpoint.baseUrl}${suffix}` : `${endpoint.baseUrl}${API_PREFIX}${suffix}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// src/config/credentials.ts
|
|
148
|
-
var REDACTED = "[redacted]";
|
|
149
|
-
var PARTNER_KEY = Object.freeze({
|
|
150
|
-
envVar: "EVERFUR_PARTNER_KEY",
|
|
151
|
-
prefix: "pk_live_",
|
|
152
|
-
// Both prefixes are 8 characters, so one expected length still covers either.
|
|
153
|
-
alsoAccepts: Object.freeze(["pk_test_"]),
|
|
154
|
-
length: "pk_live_".length + 64,
|
|
155
|
-
summary: "Publishable partner key (pk_live_... on production, pk_test_... on a sandbox). Identifies the tenant on every widget call."
|
|
156
|
-
});
|
|
157
|
-
var PARTNER_SECRET_KEY = Object.freeze({
|
|
158
|
-
envVar: "EVERFUR_PARTNER_SECRET_KEY",
|
|
159
|
-
prefix: "sk_partner_",
|
|
160
|
-
length: "sk_partner_".length + 64,
|
|
161
|
-
summary: "Partner secret key (sk_partner_...). Presented only to the session mint route."
|
|
162
|
-
});
|
|
163
|
-
var SESSION_TOKEN = Object.freeze({
|
|
164
|
-
envVar: "EVERFUR_SESSION_TOKEN",
|
|
165
|
-
prefix: null,
|
|
166
|
-
length: null,
|
|
167
|
-
summary: "An existing widget session bearer. Optional; without it diagnose uses the key path."
|
|
168
|
-
});
|
|
169
|
-
var ADMIN_TOKEN = Object.freeze({
|
|
170
|
-
envVar: "EVERFUR_ADMIN_TOKEN",
|
|
171
|
-
prefix: null,
|
|
172
|
-
length: null,
|
|
173
|
-
summary: "Admin bearer token for the /admin routes. Required by `sandbox create`."
|
|
174
|
-
});
|
|
175
|
-
var CREDENTIAL_SPECS = Object.freeze([
|
|
176
|
-
PARTNER_KEY,
|
|
177
|
-
PARTNER_SECRET_KEY,
|
|
178
|
-
SESSION_TOKEN,
|
|
179
|
-
ADMIN_TOKEN
|
|
180
|
-
]);
|
|
181
|
-
function wrap(spec, value) {
|
|
182
|
-
return Object.freeze({
|
|
183
|
-
envVar: spec.envVar,
|
|
184
|
-
toString: () => REDACTED,
|
|
185
|
-
toJSON: () => REDACTED,
|
|
186
|
-
reveal: () => value
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function ephemeralCredential(origin, value) {
|
|
190
|
-
return Object.freeze({
|
|
191
|
-
envVar: origin,
|
|
192
|
-
toString: () => REDACTED,
|
|
193
|
-
toJSON: () => REDACTED,
|
|
194
|
-
reveal: () => value
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
function withPrefix(credential, prefix) {
|
|
198
|
-
return Object.freeze({
|
|
199
|
-
envVar: credential.envVar,
|
|
200
|
-
toString: () => REDACTED,
|
|
201
|
-
toJSON: () => REDACTED,
|
|
202
|
-
reveal: () => `${prefix}${credential.reveal()}`
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
function resolveCredential(env, spec) {
|
|
206
|
-
const raw = (env[spec.envVar] ?? "").trim();
|
|
207
|
-
if (raw.length === 0) {
|
|
208
|
-
return Object.freeze({
|
|
209
|
-
spec,
|
|
210
|
-
status: "absent",
|
|
211
|
-
credential: null,
|
|
212
|
-
reason: `${spec.envVar} is not set`
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
const accepted = spec.prefix === null ? [] : [spec.prefix, ...spec.alsoAccepts ?? []];
|
|
216
|
-
if (accepted.length > 0 && !accepted.some((p) => raw.startsWith(p))) {
|
|
217
|
-
return Object.freeze({
|
|
218
|
-
spec,
|
|
219
|
-
status: "malformed",
|
|
220
|
-
credential: null,
|
|
221
|
-
reason: accepted.length === 1 ? `${spec.envVar} does not start with "${spec.prefix}"` : `${spec.envVar} does not start with any of ${accepted.map((p) => `"${p}"`).join(" or ")}`
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
if (spec.length !== null && raw.length !== spec.length) {
|
|
225
|
-
return Object.freeze({
|
|
226
|
-
spec,
|
|
227
|
-
status: "malformed",
|
|
228
|
-
credential: null,
|
|
229
|
-
reason: `${spec.envVar} is ${raw.length} characters; a ${spec.prefix ?? ""} key is ${spec.length}`
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
return Object.freeze({
|
|
233
|
-
spec,
|
|
234
|
-
status: "present",
|
|
235
|
-
credential: wrap(spec, raw),
|
|
236
|
-
reason: null
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// src/cli/json.ts
|
|
241
|
-
function isJsonObject(value) {
|
|
242
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
243
|
-
}
|
|
244
|
-
function canonicalJson(value) {
|
|
245
|
-
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
246
|
-
return JSON.stringify(value);
|
|
247
|
-
}
|
|
248
|
-
if (typeof value === "string") {
|
|
249
|
-
return JSON.stringify(value);
|
|
250
|
-
}
|
|
251
|
-
if (Array.isArray(value)) {
|
|
252
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
253
|
-
}
|
|
254
|
-
const entries = Object.entries(value);
|
|
255
|
-
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
256
|
-
const body = entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",");
|
|
257
|
-
return `{${body}}`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/detect/manifest.ts
|
|
261
|
-
function stringMap(value) {
|
|
262
|
-
if (!isJsonObject(value)) return Object.freeze({});
|
|
263
|
-
const out = {};
|
|
264
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
265
|
-
if (typeof entry === "string") out[key] = entry;
|
|
266
|
-
}
|
|
267
|
-
return Object.freeze(out);
|
|
268
|
-
}
|
|
269
|
-
function readPackageJson(fs, root) {
|
|
270
|
-
const contents = fs.readFile(join(root, "package.json"));
|
|
271
|
-
if (contents === null) return null;
|
|
272
|
-
let parsed;
|
|
273
|
-
try {
|
|
274
|
-
parsed = JSON.parse(contents);
|
|
275
|
-
} catch {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
if (!isJsonObject(parsed)) return null;
|
|
279
|
-
const packageManagerField = typeof parsed["packageManager"] === "string" ? parsed["packageManager"] : null;
|
|
280
|
-
return Object.freeze({
|
|
281
|
-
raw: parsed,
|
|
282
|
-
dependencies: stringMap(parsed["dependencies"]),
|
|
283
|
-
devDependencies: stringMap(parsed["devDependencies"]),
|
|
284
|
-
peerDependencies: stringMap(parsed["peerDependencies"]),
|
|
285
|
-
packageManagerField
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
function pubspecDependencyValue(value) {
|
|
289
|
-
if (typeof value === "string") return value;
|
|
290
|
-
if (typeof value === "number") return String(value);
|
|
291
|
-
if (isJsonObject(value)) {
|
|
292
|
-
const keys = Object.keys(value).sort();
|
|
293
|
-
if (keys.length === 0) return null;
|
|
294
|
-
return keys.map((key) => `${key}:${String(value[key])}`).join(" ");
|
|
295
|
-
}
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
function readPubspec(fs, root) {
|
|
299
|
-
const contents = fs.readFile(join(root, "pubspec.yaml"));
|
|
300
|
-
if (contents === null) return null;
|
|
301
|
-
let parsed;
|
|
302
|
-
try {
|
|
303
|
-
parsed = parse(contents);
|
|
304
|
-
} catch {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
if (!isJsonObject(parsed)) return null;
|
|
308
|
-
const environment = isJsonObject(parsed["environment"]) ? parsed["environment"] : {};
|
|
309
|
-
const dependencies = {};
|
|
310
|
-
if (isJsonObject(parsed["dependencies"])) {
|
|
311
|
-
for (const [key, entry] of Object.entries(parsed["dependencies"])) {
|
|
312
|
-
const flattened = pubspecDependencyValue(entry);
|
|
313
|
-
if (flattened !== null) dependencies[key] = flattened;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return Object.freeze({
|
|
317
|
-
name: typeof parsed["name"] === "string" ? parsed["name"] : null,
|
|
318
|
-
dartSdk: typeof environment["sdk"] === "string" ? environment["sdk"] : null,
|
|
319
|
-
flutterSdk: typeof environment["flutter"] === "string" ? environment["flutter"] : null,
|
|
320
|
-
dependencies: Object.freeze(dependencies)
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
function readInstalledVersion(fs, root, name) {
|
|
324
|
-
const contents = fs.readFile(join(root, "node_modules", ...name.split("/"), "package.json"));
|
|
325
|
-
if (contents === null) return null;
|
|
326
|
-
try {
|
|
327
|
-
const parsed = JSON.parse(contents);
|
|
328
|
-
if (!isJsonObject(parsed)) return null;
|
|
329
|
-
return typeof parsed["version"] === "string" ? parsed["version"] : null;
|
|
330
|
-
} catch {
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
var LOCKFILES = Object.freeze([
|
|
335
|
-
{ file: "package-lock.json", manager: "npm" },
|
|
336
|
-
{ file: "npm-shrinkwrap.json", manager: "npm" },
|
|
337
|
-
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
338
|
-
{ file: "yarn.lock", manager: "yarn" },
|
|
339
|
-
{ file: "bun.lockb", manager: "bun" },
|
|
340
|
-
{ file: "bun.lock", manager: "bun" }
|
|
341
|
-
]);
|
|
342
|
-
var NAMED_MANAGERS = Object.freeze(["npm", "pnpm", "yarn", "bun"]);
|
|
343
|
-
function detectPackageManager(fs, root, manifest) {
|
|
344
|
-
const field = manifest?.packageManagerField ?? null;
|
|
345
|
-
if (field !== null) {
|
|
346
|
-
const name = field.split("@")[0] ?? "";
|
|
347
|
-
const matched = NAMED_MANAGERS.find((candidate) => candidate === name);
|
|
348
|
-
if (matched !== void 0) {
|
|
349
|
-
return Object.freeze({
|
|
350
|
-
packageManager: matched,
|
|
351
|
-
evidence: Object.freeze([`package.json "packageManager" field is "${field}"`])
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
return Object.freeze({
|
|
355
|
-
packageManager: "undetermined",
|
|
356
|
-
evidence: Object.freeze([`package.json "packageManager" field "${field}" names no supported manager`])
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
const found = LOCKFILES.filter((probe) => fs.exists(join(root, probe.file)));
|
|
360
|
-
const distinct = new Set(found.map((probe) => probe.manager));
|
|
361
|
-
if (distinct.size === 1) {
|
|
362
|
-
const [only] = [...distinct];
|
|
363
|
-
if (only !== void 0) {
|
|
364
|
-
return Object.freeze({
|
|
365
|
-
packageManager: only,
|
|
366
|
-
evidence: Object.freeze(found.map((probe) => `${probe.file} present`))
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (distinct.size > 1) {
|
|
371
|
-
return Object.freeze({
|
|
372
|
-
packageManager: "undetermined",
|
|
373
|
-
evidence: Object.freeze([
|
|
374
|
-
`competing lockfiles present: ${found.map((probe) => probe.file).join(", ")}`
|
|
375
|
-
])
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
return Object.freeze({
|
|
379
|
-
packageManager: "undetermined",
|
|
380
|
-
evidence: Object.freeze(['no lockfile and no package.json "packageManager" field'])
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
var WEB_FRAMEWORK_DEPENDENCIES = Object.freeze([
|
|
384
|
-
"next",
|
|
385
|
-
"vite",
|
|
386
|
-
"@remix-run/react",
|
|
387
|
-
"@remix-run/node",
|
|
388
|
-
"astro",
|
|
389
|
-
"svelte",
|
|
390
|
-
"vue",
|
|
391
|
-
"@angular/core",
|
|
392
|
-
"react-scripts",
|
|
393
|
-
"@shopify/remix-oxygen"
|
|
394
|
-
]);
|
|
395
|
-
function allNodeDependencies(manifest) {
|
|
396
|
-
return { ...manifest.devDependencies, ...manifest.peerDependencies, ...manifest.dependencies };
|
|
397
|
-
}
|
|
398
|
-
function flutterCandidate(pubspec) {
|
|
399
|
-
if (pubspec === null) return null;
|
|
400
|
-
const evidence = ["pubspec.yaml present"];
|
|
401
|
-
if (pubspec.dependencies["flutter"] !== void 0) {
|
|
402
|
-
evidence.push("pubspec.yaml declares the flutter dependency");
|
|
403
|
-
}
|
|
404
|
-
if (pubspec.flutterSdk !== null) {
|
|
405
|
-
evidence.push(`pubspec.yaml environment.flutter is "${pubspec.flutterSdk}"`);
|
|
406
|
-
}
|
|
407
|
-
return { type: "flutter", evidence: Object.freeze(evidence) };
|
|
408
|
-
}
|
|
409
|
-
function reactNativeCandidate(fs, root, manifest) {
|
|
410
|
-
if (manifest === null) return null;
|
|
411
|
-
const dependencies = allNodeDependencies(manifest);
|
|
412
|
-
const hasExpo = dependencies["expo"] !== void 0;
|
|
413
|
-
const hasReactNative = dependencies["react-native"] !== void 0;
|
|
414
|
-
if (!hasExpo && !hasReactNative) return null;
|
|
415
|
-
const iosDirectory = fs.isDirectory(join(root, "ios"));
|
|
416
|
-
const androidDirectory = fs.isDirectory(join(root, "android"));
|
|
417
|
-
const nativeDirectories = [
|
|
418
|
-
...iosDirectory ? ["ios/"] : [],
|
|
419
|
-
...androidDirectory ? ["android/"] : []
|
|
420
|
-
];
|
|
421
|
-
const evidence = [];
|
|
422
|
-
if (hasExpo) evidence.push(`package.json declares expo "${dependencies["expo"] ?? ""}"`);
|
|
423
|
-
if (hasReactNative) evidence.push(`package.json declares react-native "${dependencies["react-native"] ?? ""}"`);
|
|
424
|
-
if (nativeDirectories.length > 0) {
|
|
425
|
-
evidence.push(`checked-in native project directories: ${nativeDirectories.join(", ")}`);
|
|
426
|
-
return { type: "bare-react-native", evidence: Object.freeze(evidence) };
|
|
427
|
-
}
|
|
428
|
-
if (hasExpo) {
|
|
429
|
-
evidence.push("no checked-in ios/ or android/ directory, so the managed workflow is intact");
|
|
430
|
-
for (const configFile of ["app.json", "app.config.js", "app.config.ts"]) {
|
|
431
|
-
if (fs.exists(join(root, configFile))) evidence.push(`${configFile} present`);
|
|
432
|
-
}
|
|
433
|
-
return { type: "expo-managed", evidence: Object.freeze(evidence) };
|
|
434
|
-
}
|
|
435
|
-
evidence.push("no checked-in ios/ or android/ directory");
|
|
436
|
-
return { type: "bare-react-native", evidence: Object.freeze(evidence) };
|
|
437
|
-
}
|
|
438
|
-
function webCandidate(fs, root, manifest) {
|
|
439
|
-
if (manifest === null) return null;
|
|
440
|
-
const dependencies = allNodeDependencies(manifest);
|
|
441
|
-
if (dependencies["react-native"] !== void 0 || dependencies["expo"] !== void 0) return null;
|
|
442
|
-
const evidence = [];
|
|
443
|
-
for (const name of WEB_FRAMEWORK_DEPENDENCIES) {
|
|
444
|
-
const declared = dependencies[name];
|
|
445
|
-
if (declared !== void 0) evidence.push(`package.json declares ${name} "${declared}"`);
|
|
446
|
-
}
|
|
447
|
-
for (const htmlFile of ["index.html", join("public", "index.html")]) {
|
|
448
|
-
if (fs.exists(join(root, htmlFile))) evidence.push(`${htmlFile.replace(/\\/g, "/")} present`);
|
|
449
|
-
}
|
|
450
|
-
if (evidence.length === 0) return null;
|
|
451
|
-
return { type: "web", evidence: Object.freeze(evidence) };
|
|
452
|
-
}
|
|
453
|
-
function detectProjectType(fs, root, manifest, pubspec) {
|
|
454
|
-
const candidates = [
|
|
455
|
-
flutterCandidate(pubspec),
|
|
456
|
-
reactNativeCandidate(fs, root, manifest),
|
|
457
|
-
webCandidate(fs, root, manifest)
|
|
458
|
-
].filter((candidate) => candidate !== null);
|
|
459
|
-
if (candidates.length === 1) {
|
|
460
|
-
const only = candidates[0];
|
|
461
|
-
if (only !== void 0) {
|
|
462
|
-
return Object.freeze({ projectType: only.type, evidence: only.evidence });
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (candidates.length > 1) {
|
|
466
|
-
return Object.freeze({
|
|
467
|
-
projectType: "unknown",
|
|
468
|
-
evidence: Object.freeze([
|
|
469
|
-
`competing project shapes detected: ${candidates.map((candidate) => candidate.type).join(", ")}`,
|
|
470
|
-
...candidates.flatMap((candidate) => candidate.evidence)
|
|
471
|
-
])
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
const evidence = [];
|
|
475
|
-
if (manifest === null && pubspec === null) {
|
|
476
|
-
evidence.push("no package.json and no pubspec.yaml at the project root");
|
|
477
|
-
} else if (manifest !== null) {
|
|
478
|
-
evidence.push("package.json present but declares no expo, react-native, or web framework dependency");
|
|
479
|
-
}
|
|
480
|
-
return Object.freeze({ projectType: "unknown", evidence: Object.freeze(evidence) });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/detect/detect.ts
|
|
484
|
-
var RELEVANT_NODE_DEPENDENCIES = Object.freeze([
|
|
485
|
-
"expo",
|
|
486
|
-
"expo-router",
|
|
487
|
-
"react",
|
|
488
|
-
"react-dom",
|
|
489
|
-
"react-native",
|
|
490
|
-
"next",
|
|
491
|
-
"@everfur/sdk"
|
|
492
|
-
]);
|
|
493
|
-
var RELEVANT_DART_DEPENDENCIES = Object.freeze(["everfur_sdk"]);
|
|
494
|
-
function detectProject(fs, root) {
|
|
495
|
-
const manifest = readPackageJson(fs, root);
|
|
496
|
-
const pubspec = readPubspec(fs, root);
|
|
497
|
-
const projectType = detectProjectType(fs, root, manifest, pubspec);
|
|
498
|
-
const dependencies = [];
|
|
499
|
-
if (manifest !== null) {
|
|
500
|
-
for (const name of RELEVANT_NODE_DEPENDENCIES) {
|
|
501
|
-
const scopes = [
|
|
502
|
-
{ scope: "dependencies", table: manifest.dependencies },
|
|
503
|
-
{ scope: "devDependencies", table: manifest.devDependencies },
|
|
504
|
-
{ scope: "peerDependencies", table: manifest.peerDependencies }
|
|
505
|
-
];
|
|
506
|
-
for (const { scope, table } of scopes) {
|
|
507
|
-
const declared = table[name];
|
|
508
|
-
if (declared === void 0) continue;
|
|
509
|
-
dependencies.push({
|
|
510
|
-
name,
|
|
511
|
-
declared,
|
|
512
|
-
installed: readInstalledVersion(fs, root, name),
|
|
513
|
-
source: "package.json",
|
|
514
|
-
scope
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (pubspec !== null) {
|
|
520
|
-
if (pubspec.dartSdk !== null) {
|
|
521
|
-
dependencies.push({
|
|
522
|
-
name: "dart",
|
|
523
|
-
declared: pubspec.dartSdk,
|
|
524
|
-
installed: null,
|
|
525
|
-
source: "pubspec.yaml",
|
|
526
|
-
scope: "sdk"
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
if (pubspec.flutterSdk !== null) {
|
|
530
|
-
dependencies.push({
|
|
531
|
-
name: "flutter",
|
|
532
|
-
declared: pubspec.flutterSdk,
|
|
533
|
-
installed: null,
|
|
534
|
-
source: "pubspec.yaml",
|
|
535
|
-
scope: "sdk"
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
for (const name of RELEVANT_DART_DEPENDENCIES) {
|
|
539
|
-
const declared = pubspec.dependencies[name];
|
|
540
|
-
if (declared === void 0) continue;
|
|
541
|
-
dependencies.push({
|
|
542
|
-
name,
|
|
543
|
-
declared,
|
|
544
|
-
installed: null,
|
|
545
|
-
source: "pubspec.yaml",
|
|
546
|
-
scope: "dependencies"
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
const packageManager = projectType.projectType === "flutter" && manifest === null ? Object.freeze({
|
|
551
|
-
packageManager: "not-applicable",
|
|
552
|
-
evidence: Object.freeze(["flutter project with no package.json, so no node package manager applies"])
|
|
553
|
-
}) : detectPackageManager(fs, root, manifest);
|
|
554
|
-
return Object.freeze({
|
|
555
|
-
root,
|
|
556
|
-
projectType: projectType.projectType,
|
|
557
|
-
projectTypeEvidence: projectType.evidence,
|
|
558
|
-
packageManager: packageManager.packageManager,
|
|
559
|
-
packageManagerEvidence: packageManager.evidence,
|
|
560
|
-
dependencies: Object.freeze(dependencies)
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// schema/plan.v1.schema.json
|
|
565
|
-
var plan_v1_schema_default = {
|
|
566
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
567
|
-
$id: "https://everfur.com/contracts/cli-plan/v1",
|
|
568
|
-
title: "Everfur CLI plan document",
|
|
569
|
-
description: "The committed output contract of `everfur plan --json`. Mirrors the TypeScript type in src/commands/plan/types.ts; tests/contract validates real emitted output against this file on every run so the two cannot drift. Compatibility rule for schemaVersion 1: fields and enum members may be ADDED; nothing is removed, renamed, retyped, or given a new meaning. Any of those is schemaVersion 2 and a new schema file. `additionalProperties` is left open on objects for exactly that reason: a consumer pinned to v1 must not break when v1.x gains a field. When the planner decision trees landed, this file gained the `blocked`, `principles` and `decisionSpace` fields, two `missingFacts[].fact` members and one `status` member, all additive; and `Capability` and `IdentityMode` stopped being enums, because the set of capabilities is published by the platform contract rather than by this CLI, and an enum here would reject a capability the platform had already shipped. That relaxation accepts every document v1 accepted and rejects nothing it used to, which is the test that keeps it inside v1.",
|
|
570
|
-
type: "object",
|
|
571
|
-
required: [
|
|
572
|
-
"schemaVersion",
|
|
573
|
-
"kind",
|
|
574
|
-
"generatedBy",
|
|
575
|
-
"status",
|
|
576
|
-
"detected",
|
|
577
|
-
"supplied",
|
|
578
|
-
"missingFacts",
|
|
579
|
-
"steps",
|
|
580
|
-
"blocked",
|
|
581
|
-
"principles",
|
|
582
|
-
"warnings",
|
|
583
|
-
"decisionSpace"
|
|
584
|
-
],
|
|
585
|
-
properties: {
|
|
586
|
-
schemaVersion: { const: 1 },
|
|
587
|
-
kind: { const: "everfur.plan" },
|
|
588
|
-
generatedBy: {
|
|
589
|
-
type: "object",
|
|
590
|
-
required: ["name", "version"],
|
|
591
|
-
properties: {
|
|
592
|
-
name: { type: "string", minLength: 1 },
|
|
593
|
-
version: { type: "string", minLength: 1 }
|
|
594
|
-
}
|
|
595
|
-
},
|
|
596
|
-
status: { enum: ["ready", "underspecified", "blocked"] },
|
|
597
|
-
detected: { $ref: "#/$defs/Detection" },
|
|
598
|
-
supplied: {
|
|
599
|
-
type: "object",
|
|
600
|
-
required: ["capabilities", "identityMode"],
|
|
601
|
-
properties: {
|
|
602
|
-
capabilities: {
|
|
603
|
-
description: "null means the flag was absent. An empty array is never emitted.",
|
|
604
|
-
oneOf: [
|
|
605
|
-
{ type: "null" },
|
|
606
|
-
{ type: "array", minItems: 1, items: { $ref: "#/$defs/Capability" }, uniqueItems: true }
|
|
607
|
-
]
|
|
608
|
-
},
|
|
609
|
-
identityMode: {
|
|
610
|
-
oneOf: [{ type: "null" }, { $ref: "#/$defs/IdentityMode" }]
|
|
611
|
-
},
|
|
612
|
-
sessionMintLanguage: {
|
|
613
|
-
description: "The backend language that mints sessions. Non-null only when the identity mode requires a server-side mint.",
|
|
614
|
-
oneOf: [{ type: "null" }, { $ref: "#/$defs/OptionId" }]
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
missingFacts: {
|
|
619
|
-
type: "array",
|
|
620
|
-
items: { $ref: "#/$defs/MissingFact" }
|
|
621
|
-
},
|
|
622
|
-
steps: {
|
|
623
|
-
type: "array",
|
|
624
|
-
items: { $ref: "#/$defs/PlanStep" }
|
|
625
|
-
},
|
|
626
|
-
blocked: {
|
|
627
|
-
description: "Capabilities the caller asked for that the contract refuses, with the reason and where to ask. Present in every status; non-empty means no step below covers those capabilities.",
|
|
628
|
-
type: "array",
|
|
629
|
-
items: { $ref: "#/$defs/BlockedCapability" }
|
|
630
|
-
},
|
|
631
|
-
principles: {
|
|
632
|
-
description: "Guidance that holds regardless of what was answered, carried in the document so it survives the loss of the context that produced the plan.",
|
|
633
|
-
type: "array",
|
|
634
|
-
items: { $ref: "#/$defs/PlanPrinciple" }
|
|
635
|
-
},
|
|
636
|
-
warnings: {
|
|
637
|
-
type: "array",
|
|
638
|
-
items: { $ref: "#/$defs/PlanWarning" }
|
|
639
|
-
},
|
|
640
|
-
decisionSpace: {
|
|
641
|
-
description: "Which contract projection produced this plan. Null only on the refusal that says none was found.",
|
|
642
|
-
oneOf: [{ type: "null" }, { $ref: "#/$defs/DecisionSpaceProvenance" }]
|
|
643
|
-
}
|
|
644
|
-
},
|
|
645
|
-
allOf: [
|
|
646
|
-
{
|
|
647
|
-
description: "status and missingFacts are two views of one fact and can never disagree.",
|
|
648
|
-
if: { properties: { status: { const: "underspecified" } }, required: ["status"] },
|
|
649
|
-
then: {
|
|
650
|
-
properties: {
|
|
651
|
-
missingFacts: { minItems: 1 },
|
|
652
|
-
steps: { maxItems: 0 }
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
description: "A ready plan has no missing facts and at least one step.",
|
|
658
|
-
if: { properties: { status: { const: "ready" } }, required: ["status"] },
|
|
659
|
-
then: {
|
|
660
|
-
properties: {
|
|
661
|
-
missingFacts: { maxItems: 0 },
|
|
662
|
-
steps: { minItems: 1 }
|
|
663
|
-
},
|
|
664
|
-
required: ["supplied", "decisionSpace"],
|
|
665
|
-
$comment: "A ready plan always carries the supplied facts and the decision space they were judged against."
|
|
666
|
-
}
|
|
667
|
-
},
|
|
668
|
-
{
|
|
669
|
-
description: "A blocked plan produced no steps and says which capabilities are blocked. An install plan for a surface that answers 404 is not a smaller plan, it is a wrong one.",
|
|
670
|
-
if: { properties: { status: { const: "blocked" } }, required: ["status"] },
|
|
671
|
-
then: {
|
|
672
|
-
properties: {
|
|
673
|
-
missingFacts: { maxItems: 0 },
|
|
674
|
-
steps: { maxItems: 0 },
|
|
675
|
-
blocked: { minItems: 1 }
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
],
|
|
680
|
-
$defs: {
|
|
681
|
-
OptionId: {
|
|
682
|
-
description: "One option id of one decision-tree question. Kebab-case, published by the contract.",
|
|
683
|
-
type: "string",
|
|
684
|
-
pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
685
|
-
},
|
|
686
|
-
Capability: {
|
|
687
|
-
description: "A capability id: one option of the capabilities tree. NOT enumerated here; the set is the platform contract's, and this CLI reads it from the projection.",
|
|
688
|
-
$ref: "#/$defs/OptionId"
|
|
689
|
-
},
|
|
690
|
-
IdentityMode: {
|
|
691
|
-
description: "An identity mode id: one option of the identity-mode tree.",
|
|
692
|
-
$ref: "#/$defs/OptionId"
|
|
693
|
-
},
|
|
694
|
-
ProjectType: {
|
|
695
|
-
enum: ["expo-managed", "bare-react-native", "flutter", "web", "unknown"]
|
|
696
|
-
},
|
|
697
|
-
PackageManager: {
|
|
698
|
-
enum: ["npm", "pnpm", "yarn", "bun", "not-applicable", "undetermined"]
|
|
699
|
-
},
|
|
700
|
-
Detection: {
|
|
701
|
-
type: "object",
|
|
702
|
-
required: [
|
|
703
|
-
"root",
|
|
704
|
-
"projectType",
|
|
705
|
-
"projectTypeEvidence",
|
|
706
|
-
"packageManager",
|
|
707
|
-
"packageManagerEvidence",
|
|
708
|
-
"dependencies"
|
|
709
|
-
],
|
|
710
|
-
properties: {
|
|
711
|
-
root: { type: "string", minLength: 1 },
|
|
712
|
-
projectType: { $ref: "#/$defs/ProjectType" },
|
|
713
|
-
projectTypeEvidence: { type: "array", items: { type: "string" } },
|
|
714
|
-
packageManager: { $ref: "#/$defs/PackageManager" },
|
|
715
|
-
packageManagerEvidence: { type: "array", items: { type: "string" } },
|
|
716
|
-
dependencies: { type: "array", items: { $ref: "#/$defs/Dependency" } }
|
|
717
|
-
}
|
|
718
|
-
},
|
|
719
|
-
Dependency: {
|
|
720
|
-
type: "object",
|
|
721
|
-
required: ["name", "declared", "installed", "source", "scope"],
|
|
722
|
-
properties: {
|
|
723
|
-
name: { type: "string", minLength: 1 },
|
|
724
|
-
declared: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
725
|
-
installed: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
726
|
-
source: { enum: ["package.json", "pubspec.yaml"] },
|
|
727
|
-
scope: { enum: ["dependencies", "devDependencies", "peerDependencies", "sdk"] }
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
BlockedCapability: {
|
|
731
|
-
type: "object",
|
|
732
|
-
required: ["capability", "reason", "unblockOwner", "requestPath", "capabilityKeys"],
|
|
733
|
-
properties: {
|
|
734
|
-
capability: { $ref: "#/$defs/Capability" },
|
|
735
|
-
reason: { type: "string", minLength: 1 },
|
|
736
|
-
unblockOwner: { type: "string", minLength: 1 },
|
|
737
|
-
requestPath: {
|
|
738
|
-
description: "Site-relative path on the partner documentation site. Resolved against the generated site when the contract is compiled, so it cannot point at a page that does not exist.",
|
|
739
|
-
type: "string",
|
|
740
|
-
pattern: "^/"
|
|
741
|
-
},
|
|
742
|
-
capabilityKeys: {
|
|
743
|
-
description: "The capability pack keys this block derives from.",
|
|
744
|
-
type: "array",
|
|
745
|
-
items: { type: "string", minLength: 1 }
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
},
|
|
749
|
-
PlanPrinciple: {
|
|
750
|
-
type: "object",
|
|
751
|
-
required: ["treeId", "id", "title", "statement", "docPath"],
|
|
752
|
-
properties: {
|
|
753
|
-
treeId: { $ref: "#/$defs/OptionId" },
|
|
754
|
-
id: { $ref: "#/$defs/OptionId" },
|
|
755
|
-
title: { type: "string", minLength: 1 },
|
|
756
|
-
statement: { type: "string", minLength: 1 },
|
|
757
|
-
docPath: { oneOf: [{ type: "null" }, { type: "string", pattern: "^/" }] }
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
DecisionSpaceProvenance: {
|
|
761
|
-
type: "object",
|
|
762
|
-
required: ["source", "digest", "trees"],
|
|
763
|
-
properties: {
|
|
764
|
-
source: { type: "string", minLength: 1 },
|
|
765
|
-
digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
766
|
-
trees: { type: "array", items: { $ref: "#/$defs/OptionId" } }
|
|
767
|
-
}
|
|
768
|
-
},
|
|
769
|
-
MissingFact: {
|
|
770
|
-
type: "object",
|
|
771
|
-
required: ["fact", "reason", "supplyWith"],
|
|
772
|
-
properties: {
|
|
773
|
-
fact: {
|
|
774
|
-
enum: [
|
|
775
|
-
"decisionTrees",
|
|
776
|
-
"projectType",
|
|
777
|
-
"packageManager",
|
|
778
|
-
"capabilities",
|
|
779
|
-
"identityMode",
|
|
780
|
-
"sessionMintLanguage"
|
|
781
|
-
]
|
|
782
|
-
},
|
|
783
|
-
reason: { type: "string", minLength: 1 },
|
|
784
|
-
supplyWith: { type: "string", minLength: 1 }
|
|
785
|
-
}
|
|
786
|
-
},
|
|
787
|
-
PlanStep: {
|
|
788
|
-
type: "object",
|
|
789
|
-
required: ["id", "phase", "title", "detail", "commands", "files"],
|
|
790
|
-
properties: {
|
|
791
|
-
id: { type: "string", pattern: "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
792
|
-
phase: { enum: ["install", "native", "configure", "code", "verify"] },
|
|
793
|
-
title: { type: "string", minLength: 1 },
|
|
794
|
-
detail: { type: "string", minLength: 1 },
|
|
795
|
-
commands: { type: "array", items: { type: "string", minLength: 1 } },
|
|
796
|
-
files: { type: "array", items: { type: "string", minLength: 1 } }
|
|
797
|
-
}
|
|
798
|
-
},
|
|
799
|
-
PlanWarning: {
|
|
800
|
-
type: "object",
|
|
801
|
-
required: ["code", "message"],
|
|
802
|
-
properties: {
|
|
803
|
-
code: { type: "string", pattern: "^[a-z0-9]+(_[a-z0-9]+)*$" },
|
|
804
|
-
message: { type: "string", minLength: 1 }
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
// schema/contract-public.v1.schema.json
|
|
811
|
-
var contract_public_v1_schema_default = {
|
|
812
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
813
|
-
$id: "https://everfur.com/contracts/contract-public/v1",
|
|
814
|
-
title: "Everfur contract-public projection envelope",
|
|
815
|
-
description: "The ENVELOPE around a contract-public projection. This CLI owns the envelope and the digest rule; it deliberately does NOT own the shape of `projection`, which is produced by the contract lane. Validating the payload here would fork the definition and guarantee the two drift. `projection` is therefore required to be an object and is otherwise opaque, and the digest is what proves it arrived intact. When the contract lane publishes its own payload schema, add it as a $ref under projection and bump to v2 only if that tightening rejects a document v1 accepted.",
|
|
816
|
-
type: "object",
|
|
817
|
-
required: ["schemaVersion", "kind", "generatedAt", "digest", "projection"],
|
|
818
|
-
properties: {
|
|
819
|
-
schemaVersion: { const: 1 },
|
|
820
|
-
kind: { const: "everfur.contract-public" },
|
|
821
|
-
generatedAt: {
|
|
822
|
-
type: "string",
|
|
823
|
-
format: "date-time",
|
|
824
|
-
description: "RFC 3339 timestamp. Outside the digest input, so re-stamping a projection does not invalidate it."
|
|
825
|
-
},
|
|
826
|
-
digest: {
|
|
827
|
-
type: "object",
|
|
828
|
-
required: ["algorithm", "value"],
|
|
829
|
-
properties: {
|
|
830
|
-
algorithm: {
|
|
831
|
-
const: "sha256",
|
|
832
|
-
description: "Only sha256 is defined for v1. A new algorithm is an added enum member, which v1 permits."
|
|
833
|
-
},
|
|
834
|
-
value: {
|
|
835
|
-
type: "string",
|
|
836
|
-
pattern: "^[0-9a-f]{64}$",
|
|
837
|
-
description: "Lowercase hex sha256 over the RFC 8785 style canonical JSON of `projection` alone."
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
},
|
|
841
|
-
projection: {
|
|
842
|
-
type: "object",
|
|
843
|
-
description: "Opaque to this CLI. Owned by the contract lane."
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
// schema/diagnosis.v1.schema.json
|
|
849
|
-
var diagnosis_v1_schema_default = {
|
|
850
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
851
|
-
$id: "https://everfur.com/contracts/cli-diagnosis/v1",
|
|
852
|
-
title: "Everfur CLI diagnosis document",
|
|
853
|
-
description: "The committed output contract of `everfur diagnose --json`. Mirrors src/commands/diagnose/document.ts; tests/contract validates real emitted output against this file on every run so the two cannot drift. Compatibility rule for schemaVersion 1: fields and enum members may be ADDED; nothing is removed, renamed, retyped, or given a new meaning. reasonCode is deliberately NOT an enum: the platform's DiagnosticsReasonCode grows when a new decision point is added, and a CLI that rejected an unrecognised cause would fail exactly when something new broke. additionalProperties is left open for the same reason.",
|
|
854
|
-
type: "object",
|
|
855
|
-
required: [
|
|
856
|
-
"schemaVersion",
|
|
857
|
-
"kind",
|
|
858
|
-
"generatedBy",
|
|
859
|
-
"endpoint",
|
|
860
|
-
"requestId",
|
|
861
|
-
"evaluatedAt",
|
|
862
|
-
"client",
|
|
863
|
-
"auth",
|
|
864
|
-
"scope",
|
|
865
|
-
"summary",
|
|
866
|
-
"capabilities"
|
|
867
|
-
],
|
|
868
|
-
properties: {
|
|
869
|
-
schemaVersion: { const: 1 },
|
|
870
|
-
kind: { const: "everfur.diagnosis" },
|
|
871
|
-
generatedBy: { $ref: "#/$defs/GeneratedBy" },
|
|
872
|
-
endpoint: {
|
|
873
|
-
type: "object",
|
|
874
|
-
required: ["baseUrl", "path"],
|
|
875
|
-
properties: {
|
|
876
|
-
baseUrl: { type: "string", minLength: 1 },
|
|
877
|
-
path: { type: "string", minLength: 1 }
|
|
878
|
-
}
|
|
879
|
-
},
|
|
880
|
-
requestId: {
|
|
881
|
-
description: "The X-Request-ID of the diagnosing call. Null only when the response carried none.",
|
|
882
|
-
oneOf: [{ type: "null" }, { type: "string", minLength: 1 }]
|
|
883
|
-
},
|
|
884
|
-
evaluatedAt: { type: "string" },
|
|
885
|
-
client: {
|
|
886
|
-
type: "object",
|
|
887
|
-
required: ["id", "slug"],
|
|
888
|
-
properties: {
|
|
889
|
-
id: { type: "string" },
|
|
890
|
-
slug: { type: "string" }
|
|
891
|
-
}
|
|
892
|
-
},
|
|
893
|
-
auth: {
|
|
894
|
-
type: "object",
|
|
895
|
-
required: [
|
|
896
|
-
"mode",
|
|
897
|
-
"anonymous",
|
|
898
|
-
"userRefPrefix",
|
|
899
|
-
"userRole",
|
|
900
|
-
"roleSource",
|
|
901
|
-
"sessionId",
|
|
902
|
-
"partnerKeyScopeResolved"
|
|
903
|
-
],
|
|
904
|
-
properties: {
|
|
905
|
-
mode: { type: "string" },
|
|
906
|
-
anonymous: { type: "boolean" },
|
|
907
|
-
userRefPrefix: {
|
|
908
|
-
description: "At most the first 8 characters of the resolved user ref. Never a credential.",
|
|
909
|
-
type: "string",
|
|
910
|
-
maxLength: 8
|
|
911
|
-
},
|
|
912
|
-
userRole: { type: "string" },
|
|
913
|
-
roleSource: { type: "string" },
|
|
914
|
-
sessionId: { type: "string" },
|
|
915
|
-
partnerKeyScopeResolved: { type: "boolean" }
|
|
916
|
-
}
|
|
917
|
-
},
|
|
918
|
-
scope: {
|
|
919
|
-
type: "object",
|
|
920
|
-
required: ["provisioned", "resolverStatus", "revision", "region", "levels"],
|
|
921
|
-
properties: {
|
|
922
|
-
provisioned: { type: "boolean" },
|
|
923
|
-
resolverStatus: { type: "string" },
|
|
924
|
-
revision: { type: "integer", minimum: 0 },
|
|
925
|
-
region: { type: "string" },
|
|
926
|
-
levels: {
|
|
927
|
-
type: "array",
|
|
928
|
-
items: {
|
|
929
|
-
type: "object",
|
|
930
|
-
required: ["level", "kind", "present"],
|
|
931
|
-
properties: {
|
|
932
|
-
level: { type: "integer", minimum: 1, maximum: 5 },
|
|
933
|
-
kind: { type: "string" },
|
|
934
|
-
present: { type: "boolean" }
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
},
|
|
940
|
-
summary: {
|
|
941
|
-
type: "object",
|
|
942
|
-
required: ["total", "available", "unavailable", "byReasonCode"],
|
|
943
|
-
properties: {
|
|
944
|
-
total: { type: "integer", minimum: 0 },
|
|
945
|
-
available: { type: "integer", minimum: 0 },
|
|
946
|
-
unavailable: { type: "integer", minimum: 0 },
|
|
947
|
-
byReasonCode: {
|
|
948
|
-
description: "Cause code to how many capabilities carry it. The field that turns eight symptoms into one ticket.",
|
|
949
|
-
type: "object",
|
|
950
|
-
additionalProperties: { type: "integer", minimum: 1 }
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
},
|
|
954
|
-
capabilities: {
|
|
955
|
-
type: "array",
|
|
956
|
-
items: { $ref: "#/$defs/CapabilityDiagnosis" }
|
|
957
|
-
}
|
|
958
|
-
},
|
|
959
|
-
$defs: {
|
|
960
|
-
GeneratedBy: {
|
|
961
|
-
type: "object",
|
|
962
|
-
required: ["name", "version"],
|
|
963
|
-
properties: {
|
|
964
|
-
name: { type: "string", minLength: 1 },
|
|
965
|
-
version: { type: "string", minLength: 1 }
|
|
966
|
-
}
|
|
967
|
-
},
|
|
968
|
-
CapabilityDiagnosis: {
|
|
969
|
-
type: "object",
|
|
970
|
-
required: [
|
|
971
|
-
"key",
|
|
972
|
-
"available",
|
|
973
|
-
"reasonCode",
|
|
974
|
-
"reason",
|
|
975
|
-
"entitlementReasonCode",
|
|
976
|
-
"decidedAtLevel",
|
|
977
|
-
"decidedAtKind",
|
|
978
|
-
"surface",
|
|
979
|
-
"nextAction"
|
|
980
|
-
],
|
|
981
|
-
properties: {
|
|
982
|
-
key: { type: "string", minLength: 1 },
|
|
983
|
-
available: { type: "boolean" },
|
|
984
|
-
reasonCode: {
|
|
985
|
-
description: "The EFFECTIVE cause: the fence that blocks this key, so it names what has to be fixed first.",
|
|
986
|
-
type: "string",
|
|
987
|
-
minLength: 1
|
|
988
|
-
},
|
|
989
|
-
reason: {
|
|
990
|
-
description: "The server's own static copy for the code. Never operator or upstream text.",
|
|
991
|
-
type: "string"
|
|
992
|
-
},
|
|
993
|
-
entitlementReasonCode: {
|
|
994
|
-
description: "What the entitlement lattice alone decided, ignoring runtime gates. Differs from reasonCode only when a gate masks a lattice outcome.",
|
|
995
|
-
type: "string",
|
|
996
|
-
minLength: 1
|
|
997
|
-
},
|
|
998
|
-
decidedAtLevel: {
|
|
999
|
-
oneOf: [{ type: "null" }, { type: "integer", minimum: 1, maximum: 5 }]
|
|
1000
|
-
},
|
|
1001
|
-
decidedAtKind: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
1002
|
-
surface: {
|
|
1003
|
-
description: "The runtime-gated surface name, disclosed only to a caller the lattice grants. Null otherwise.",
|
|
1004
|
-
oneOf: [{ type: "null" }, { type: "string" }]
|
|
1005
|
-
},
|
|
1006
|
-
nextAction: {
|
|
1007
|
-
description: "CLI-authored. What the partner should do about this cause, which the server cannot know.",
|
|
1008
|
-
type: "string",
|
|
1009
|
-
minLength: 1
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
// schema/sandbox-create.v1.schema.json
|
|
1017
|
-
var sandbox_create_v1_schema_default = {
|
|
1018
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1019
|
-
$id: "https://everfur.com/contracts/cli-sandbox-create/v1",
|
|
1020
|
-
title: "Everfur CLI sandbox create document",
|
|
1021
|
-
description: "The committed output contract of `everfur sandbox create --json`. The load-bearing clause is `secretDisclosed: {const: false}`: this document may never carry a raw credential, and a machine reading it can assert that rather than trusting prose. Any change that made the field true would fail tests/contract before it shipped. `status: claim_unavailable` is a complete, valid document, not an error: the key WAS provisioned and there is simply no way to claim its secret, which exits 8.",
|
|
1022
|
-
type: "object",
|
|
1023
|
-
required: [
|
|
1024
|
-
"schemaVersion",
|
|
1025
|
-
"kind",
|
|
1026
|
-
"generatedBy",
|
|
1027
|
-
"status",
|
|
1028
|
-
"requestId",
|
|
1029
|
-
"clientId",
|
|
1030
|
-
"keyId",
|
|
1031
|
-
"label",
|
|
1032
|
-
"createdAt",
|
|
1033
|
-
"claimUrl",
|
|
1034
|
-
"claimExpiresAt",
|
|
1035
|
-
"secretDisclosed",
|
|
1036
|
-
"secretDiscarded",
|
|
1037
|
-
"orphanedKey",
|
|
1038
|
-
"nextActions"
|
|
1039
|
-
],
|
|
1040
|
-
properties: {
|
|
1041
|
-
schemaVersion: { const: 1 },
|
|
1042
|
-
kind: { const: "everfur.sandbox-create" },
|
|
1043
|
-
generatedBy: {
|
|
1044
|
-
type: "object",
|
|
1045
|
-
required: ["name", "version"],
|
|
1046
|
-
properties: {
|
|
1047
|
-
name: { type: "string", minLength: 1 },
|
|
1048
|
-
version: { type: "string", minLength: 1 }
|
|
1049
|
-
}
|
|
1050
|
-
},
|
|
1051
|
-
status: { enum: ["claim_issued", "claim_unavailable"] },
|
|
1052
|
-
requestId: { oneOf: [{ type: "null" }, { type: "string", minLength: 1 }] },
|
|
1053
|
-
clientId: { type: "string" },
|
|
1054
|
-
keyId: { type: "string", minLength: 1 },
|
|
1055
|
-
label: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
1056
|
-
createdAt: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
1057
|
-
claimUrl: {
|
|
1058
|
-
description: "Where a human claims the secret in a browser. Null exactly when status is claim_unavailable.",
|
|
1059
|
-
oneOf: [{ type: "null" }, { type: "string", minLength: 1 }]
|
|
1060
|
-
},
|
|
1061
|
-
claimExpiresAt: { oneOf: [{ type: "null" }, { type: "string" }] },
|
|
1062
|
-
secretDisclosed: {
|
|
1063
|
-
description: "Always false. The CLI does not print, log, or return a raw secret in any mode.",
|
|
1064
|
-
const: false
|
|
1065
|
-
},
|
|
1066
|
-
secretDiscarded: {
|
|
1067
|
-
description: "True when the server returned a raw secret inline and the CLI discarded it unread, which leaves a live credential nobody holds.",
|
|
1068
|
-
type: "boolean"
|
|
1069
|
-
},
|
|
1070
|
-
orphanedKey: {
|
|
1071
|
-
description: "What became of an unclaimable key. `not_needed` means a claim URL was issued and the key is wanted. `revoked` means the CLI minted an unclaimable key and revoked it again immediately, leaving nothing live behind. `revoke_failed` is the ONLY value that means a live credential nobody holds still exists.",
|
|
1072
|
-
enum: ["not_needed", "revoked", "revoke_failed"]
|
|
1073
|
-
},
|
|
1074
|
-
nextActions: {
|
|
1075
|
-
type: "array",
|
|
1076
|
-
minItems: 1,
|
|
1077
|
-
items: { type: "string", minLength: 1 }
|
|
1078
|
-
}
|
|
1079
|
-
},
|
|
1080
|
-
allOf: [
|
|
1081
|
-
{
|
|
1082
|
-
if: { properties: { status: { const: "claim_unavailable" } }, required: ["status"] },
|
|
1083
|
-
then: { properties: { claimUrl: { type: "null" } } }
|
|
1084
|
-
},
|
|
1085
|
-
{
|
|
1086
|
-
if: { properties: { status: { const: "claim_issued" } }, required: ["status"] },
|
|
1087
|
-
then: { properties: { claimUrl: { type: "string", minLength: 1 } } }
|
|
1088
|
-
}
|
|
1089
|
-
]
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
// schema/verification.v1.schema.json
|
|
1093
|
-
var verification_v1_schema_default = {
|
|
1094
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1095
|
-
$id: "https://everfur.com/contracts/cli-verification/v1",
|
|
1096
|
-
title: "Everfur CLI verification document",
|
|
1097
|
-
description: "The committed output contract of `everfur verify --json`. Every step carries a result AND a reason, including a passing one: this command never reports a bare exit code as its result, and `reason` having minLength 1 on every step is that rule expressed where a machine can check it. `requestId` is null only for a step that made no call, which is what makes a SKIP legible in a support ticket.",
|
|
1098
|
-
type: "object",
|
|
1099
|
-
required: ["schemaVersion", "kind", "generatedBy", "status", "baseUrl", "summary", "steps"],
|
|
1100
|
-
properties: {
|
|
1101
|
-
schemaVersion: { const: 1 },
|
|
1102
|
-
kind: { const: "everfur.verification" },
|
|
1103
|
-
generatedBy: {
|
|
1104
|
-
type: "object",
|
|
1105
|
-
required: ["name", "version"],
|
|
1106
|
-
properties: {
|
|
1107
|
-
name: { type: "string", minLength: 1 },
|
|
1108
|
-
version: { type: "string", minLength: 1 }
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
status: {
|
|
1112
|
-
description: "passed = every step PASSed. failed = at least one FAILed (exit 7). incomplete = nothing failed but a step was skipped (exit 3).",
|
|
1113
|
-
enum: ["passed", "failed", "incomplete"]
|
|
1114
|
-
},
|
|
1115
|
-
baseUrl: { type: "string", minLength: 1 },
|
|
1116
|
-
summary: {
|
|
1117
|
-
type: "object",
|
|
1118
|
-
required: ["passed", "failed", "skipped"],
|
|
1119
|
-
properties: {
|
|
1120
|
-
passed: { type: "integer", minimum: 0 },
|
|
1121
|
-
failed: { type: "integer", minimum: 0 },
|
|
1122
|
-
skipped: { type: "integer", minimum: 0 }
|
|
1123
|
-
}
|
|
1124
|
-
},
|
|
1125
|
-
steps: {
|
|
1126
|
-
type: "array",
|
|
1127
|
-
minItems: 1,
|
|
1128
|
-
items: {
|
|
1129
|
-
type: "object",
|
|
1130
|
-
required: ["id", "title", "result", "reason", "requestId", "details"],
|
|
1131
|
-
properties: {
|
|
1132
|
-
id: { type: "string", minLength: 1 },
|
|
1133
|
-
title: { type: "string", minLength: 1 },
|
|
1134
|
-
result: { enum: ["PASS", "FAIL", "SKIP"] },
|
|
1135
|
-
reason: {
|
|
1136
|
-
description: "Always populated, for every result. A bare PASS is not a report.",
|
|
1137
|
-
type: "string",
|
|
1138
|
-
minLength: 1
|
|
1139
|
-
},
|
|
1140
|
-
requestId: { oneOf: [{ type: "null" }, { type: "string", minLength: 1 }] },
|
|
1141
|
-
details: { type: "array", items: { type: "string" } }
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
// schema/error-explanation.v1.schema.json
|
|
1149
|
-
var error_explanation_v1_schema_default = {
|
|
1150
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1151
|
-
$id: "https://everfur.com/contracts/cli-error-explanation/v1",
|
|
1152
|
-
title: "Everfur CLI error explanation document",
|
|
1153
|
-
description: "The committed output contract of `everfur explain-error <code> --json`. Every field except `nextAction` is read from the contract-public projection named in `source`, and `source.digest` is the sha256 that projection was verified against, so a reader can prove which registry an explanation came from. `nextAction` is CLI-authored and derived from retryable plus httpStatus, which is why a code added to the registry tomorrow already gets a correct action today.",
|
|
1154
|
-
type: "object",
|
|
1155
|
-
required: [
|
|
1156
|
-
"schemaVersion",
|
|
1157
|
-
"kind",
|
|
1158
|
-
"generatedBy",
|
|
1159
|
-
"code",
|
|
1160
|
-
"source",
|
|
1161
|
-
"envelope",
|
|
1162
|
-
"httpStatus",
|
|
1163
|
-
"namingFamily",
|
|
1164
|
-
"retryable",
|
|
1165
|
-
"confirmedOnWire",
|
|
1166
|
-
"surfaces",
|
|
1167
|
-
"cause",
|
|
1168
|
-
"display",
|
|
1169
|
-
"docUrl",
|
|
1170
|
-
"nextAction"
|
|
1171
|
-
],
|
|
1172
|
-
properties: {
|
|
1173
|
-
schemaVersion: { const: 1 },
|
|
1174
|
-
kind: { const: "everfur.error-explanation" },
|
|
1175
|
-
generatedBy: {
|
|
1176
|
-
type: "object",
|
|
1177
|
-
required: ["name", "version"],
|
|
1178
|
-
properties: {
|
|
1179
|
-
name: { type: "string", minLength: 1 },
|
|
1180
|
-
version: { type: "string", minLength: 1 }
|
|
1181
|
-
}
|
|
1182
|
-
},
|
|
1183
|
-
code: {
|
|
1184
|
-
description: "Exactly the string the wire carries. Not normalised: three naming families are live at once and a client branches on the literal.",
|
|
1185
|
-
type: "string",
|
|
1186
|
-
minLength: 1
|
|
1187
|
-
},
|
|
1188
|
-
source: {
|
|
1189
|
-
type: "object",
|
|
1190
|
-
required: ["path", "digest"],
|
|
1191
|
-
properties: {
|
|
1192
|
-
path: { type: "string", minLength: 1 },
|
|
1193
|
-
digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
|
|
1194
|
-
}
|
|
1195
|
-
},
|
|
1196
|
-
envelope: { type: "string", minLength: 1 },
|
|
1197
|
-
httpStatus: { type: "integer", minimum: 0 },
|
|
1198
|
-
namingFamily: { type: "string", minLength: 1 },
|
|
1199
|
-
retryable: { type: "boolean" },
|
|
1200
|
-
confirmedOnWire: {
|
|
1201
|
-
description: "False means the code exists in source but no live route was traced emitting it. Documentation, not a contract to branch on.",
|
|
1202
|
-
type: "boolean"
|
|
1203
|
-
},
|
|
1204
|
-
surfaces: { type: "array", items: { type: "string" } },
|
|
1205
|
-
cause: { type: "string" },
|
|
1206
|
-
display: {
|
|
1207
|
-
description: "Safe copy a client may show a person as-is, authored by the contract lane.",
|
|
1208
|
-
type: "string"
|
|
1209
|
-
},
|
|
1210
|
-
docUrl: { type: "string" },
|
|
1211
|
-
nextAction: { type: "string", minLength: 1 }
|
|
1212
|
-
}
|
|
1213
|
-
};
|
|
1214
|
-
|
|
1215
|
-
// src/schema/validate.ts
|
|
1216
|
-
var ajv = new Ajv2020({ allErrors: true, strict: false, allowUnionTypes: true });
|
|
1217
|
-
addFormats(ajv);
|
|
1218
|
-
var compiled = /* @__PURE__ */ new Map();
|
|
1219
|
-
function compile(key, schema) {
|
|
1220
|
-
const existing = compiled.get(key);
|
|
1221
|
-
if (existing !== void 0) return existing;
|
|
1222
|
-
const validator = ajv.compile(schema);
|
|
1223
|
-
compiled.set(key, validator);
|
|
1224
|
-
return validator;
|
|
1225
|
-
}
|
|
1226
|
-
function toViolations(errors) {
|
|
1227
|
-
if (errors === null || errors === void 0) return [];
|
|
1228
|
-
return errors.map((error) => ({
|
|
1229
|
-
path: error.instancePath === "" ? "/" : error.instancePath,
|
|
1230
|
-
message: error.message ?? "failed validation"
|
|
1231
|
-
}));
|
|
1232
|
-
}
|
|
1233
|
-
function run(key, schema, document) {
|
|
1234
|
-
const validator = compile(key, schema);
|
|
1235
|
-
const valid = validator(document);
|
|
1236
|
-
return Object.freeze({
|
|
1237
|
-
valid: valid === true,
|
|
1238
|
-
violations: valid === true ? Object.freeze([]) : toViolations(validator.errors)
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
function validatePlanDocument(document) {
|
|
1242
|
-
return run("plan.v1", plan_v1_schema_default, document);
|
|
1243
|
-
}
|
|
1244
|
-
function validateContractPublicEnvelope(document) {
|
|
1245
|
-
return run("contract-public.v1", contract_public_v1_schema_default, document);
|
|
1246
|
-
}
|
|
1247
|
-
function validateDiagnosisDocument(document) {
|
|
1248
|
-
return run("diagnosis.v1", diagnosis_v1_schema_default, document);
|
|
1249
|
-
}
|
|
1250
|
-
function validateSandboxCreateDocument(document) {
|
|
1251
|
-
return run("sandbox-create.v1", sandbox_create_v1_schema_default, document);
|
|
1252
|
-
}
|
|
1253
|
-
function validateVerificationDocument(document) {
|
|
1254
|
-
return run("verification.v1", verification_v1_schema_default, document);
|
|
1255
|
-
}
|
|
1256
|
-
function validateErrorExplanationDocument(document) {
|
|
1257
|
-
return run("error-explanation.v1", error_explanation_v1_schema_default, document);
|
|
1258
|
-
}
|
|
1259
|
-
var SCHEMAS = Object.freeze({
|
|
1260
|
-
plan: plan_v1_schema_default,
|
|
1261
|
-
contractPublic: contract_public_v1_schema_default,
|
|
1262
|
-
diagnosis: diagnosis_v1_schema_default,
|
|
1263
|
-
sandboxCreate: sandbox_create_v1_schema_default,
|
|
1264
|
-
verification: verification_v1_schema_default,
|
|
1265
|
-
errorExplanation: error_explanation_v1_schema_default
|
|
1266
|
-
});
|
|
1267
|
-
function digestOfProjection(projection) {
|
|
1268
|
-
return createHash("sha256").update(canonicalJson(projection), "utf8").digest("hex");
|
|
1269
|
-
}
|
|
1270
|
-
function digestsEqual(left, right) {
|
|
1271
|
-
if (left.length !== right.length) return false;
|
|
1272
|
-
let difference = 0;
|
|
1273
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
1274
|
-
difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
1275
|
-
}
|
|
1276
|
-
return difference === 0;
|
|
1277
|
-
}
|
|
1278
|
-
function verifyDigest(projection, declared) {
|
|
1279
|
-
const actual = digestOfProjection(projection);
|
|
1280
|
-
return Object.freeze({
|
|
1281
|
-
matches: digestsEqual(declared.toLowerCase(), actual),
|
|
1282
|
-
expected: declared.toLowerCase(),
|
|
1283
|
-
actual
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
function resolveContractPath(cwd, path) {
|
|
1287
|
-
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
1288
|
-
}
|
|
1289
|
-
function validateContractFile(fs, path) {
|
|
1290
|
-
const contents = fs.readFile(path);
|
|
1291
|
-
if (contents === null) {
|
|
1292
|
-
return { kind: "file_not_found", path };
|
|
1293
|
-
}
|
|
1294
|
-
let parsed;
|
|
1295
|
-
try {
|
|
1296
|
-
parsed = JSON.parse(contents);
|
|
1297
|
-
} catch (cause) {
|
|
1298
|
-
return {
|
|
1299
|
-
kind: "invalid_json",
|
|
1300
|
-
path,
|
|
1301
|
-
detail: cause instanceof Error ? cause.message : "could not be parsed as JSON"
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
const schema = validateContractPublicEnvelope(parsed);
|
|
1305
|
-
if (!schema.valid) {
|
|
1306
|
-
return { kind: "invalid_contract", path, violations: schema.violations };
|
|
1307
|
-
}
|
|
1308
|
-
if (!isJsonObject(parsed)) {
|
|
1309
|
-
return { kind: "invalid_contract", path, violations: [{ path: "/", message: "expected an object" }] };
|
|
1310
|
-
}
|
|
1311
|
-
const projection = parsed["projection"];
|
|
1312
|
-
const digestField = parsed["digest"];
|
|
1313
|
-
if (!isJsonObject(projection) || !isJsonObject(digestField)) {
|
|
1314
|
-
return { kind: "invalid_contract", path, violations: [{ path: "/projection", message: "expected an object" }] };
|
|
1315
|
-
}
|
|
1316
|
-
const declared = digestField["value"];
|
|
1317
|
-
if (typeof declared !== "string") {
|
|
1318
|
-
return { kind: "invalid_contract", path, violations: [{ path: "/digest/value", message: "expected a string" }] };
|
|
1319
|
-
}
|
|
1320
|
-
const digest = verifyDigest(projection, declared);
|
|
1321
|
-
if (!digest.matches) {
|
|
1322
|
-
return { kind: "digest_mismatch", path, digest };
|
|
1323
|
-
}
|
|
1324
|
-
const generatedAt = typeof parsed["generatedAt"] === "string" ? parsed["generatedAt"] : "";
|
|
1325
|
-
return {
|
|
1326
|
-
kind: "valid",
|
|
1327
|
-
path,
|
|
1328
|
-
digest,
|
|
1329
|
-
generatedAt,
|
|
1330
|
-
projectionKeys: Object.freeze(Object.keys(projection).sort()),
|
|
1331
|
-
/* Same narrowing the digest above relies on: past the schema gate this is JSON. */
|
|
1332
|
-
projection
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// src/registry/errors.ts
|
|
1337
|
-
function buildRegistry(entries) {
|
|
1338
|
-
const byCode = new Map(entries.map((entry) => [entry.code, entry]));
|
|
1339
|
-
return Object.freeze({
|
|
1340
|
-
lookup: (code) => byCode.get(code) ?? null,
|
|
1341
|
-
codes: Object.freeze([...byCode.keys()].sort())
|
|
1342
|
-
});
|
|
1343
|
-
}
|
|
1344
|
-
var EMPTY_REGISTRY = buildRegistry([]);
|
|
1345
|
-
var ERRORS_DOCUMENT_KEY = "errors.yaml";
|
|
1346
|
-
function readString(record, key) {
|
|
1347
|
-
const value = record[key];
|
|
1348
|
-
return typeof value === "string" ? value : null;
|
|
1349
|
-
}
|
|
1350
|
-
function readStringList(record, key) {
|
|
1351
|
-
const value = record[key];
|
|
1352
|
-
if (!Array.isArray(value)) return [];
|
|
1353
|
-
return Object.freeze(value.filter((item) => typeof item === "string"));
|
|
1354
|
-
}
|
|
1355
|
-
function entryFromRow(row) {
|
|
1356
|
-
if (!isJsonObject(row)) return null;
|
|
1357
|
-
const code = readString(row, "code");
|
|
1358
|
-
if (code === null || code.length === 0) return null;
|
|
1359
|
-
const status = row["http_status"];
|
|
1360
|
-
return Object.freeze({
|
|
1361
|
-
code,
|
|
1362
|
-
envelope: readString(row, "envelope") ?? "unknown",
|
|
1363
|
-
httpStatus: typeof status === "number" ? status : 0,
|
|
1364
|
-
namingFamily: readString(row, "naming_family") ?? "unknown",
|
|
1365
|
-
retryable: row["retryable"] === true,
|
|
1366
|
-
confirmedOnWire: row["confirmed_on_wire"] === true,
|
|
1367
|
-
surfaces: readStringList(row, "surfaces"),
|
|
1368
|
-
display: readString(row, "display") ?? "",
|
|
1369
|
-
docUrl: readString(row, "doc_url") ?? "",
|
|
1370
|
-
note: readString(row, "note") ?? ""
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
function loadErrorRegistry(fs, path) {
|
|
1374
|
-
const outcome = validateContractFile(fs, path);
|
|
1375
|
-
if (outcome.kind !== "valid") return { kind: "contract_rejected", outcome };
|
|
1376
|
-
const document = outcome.projection[ERRORS_DOCUMENT_KEY];
|
|
1377
|
-
if (!isJsonObject(document)) return { kind: "no_registry", path };
|
|
1378
|
-
const rows = document["errors"];
|
|
1379
|
-
if (!Array.isArray(rows)) return { kind: "no_registry", path };
|
|
1380
|
-
const entries = rows.map(entryFromRow).filter((entry) => entry !== null);
|
|
1381
|
-
if (entries.length === 0) return { kind: "no_registry", path };
|
|
1382
|
-
return {
|
|
1383
|
-
kind: "loaded",
|
|
1384
|
-
registry: buildRegistry(entries),
|
|
1385
|
-
path,
|
|
1386
|
-
digest: outcome.digest.actual
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// src/http/client.ts
|
|
1391
|
-
var CORRELATABLE_REQUEST_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32})$/i;
|
|
1392
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1393
|
-
var MIN_TIMEOUT_MS = 250;
|
|
1394
|
-
var MAX_TIMEOUT_MS = 12e4;
|
|
1395
|
-
var RETRYABLE_STATUSES = Object.freeze([429, 502, 503, 504]);
|
|
1396
|
-
function clampTimeout(value) {
|
|
1397
|
-
if (!Number.isFinite(value)) return DEFAULT_TIMEOUT_MS;
|
|
1398
|
-
return Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.trunc(value)));
|
|
1399
|
-
}
|
|
1400
|
-
function isCorrelatableRequestId(value) {
|
|
1401
|
-
return CORRELATABLE_REQUEST_ID.test(value);
|
|
1402
|
-
}
|
|
1403
|
-
function requestIdOf(headers, body) {
|
|
1404
|
-
const header = headers["x-request-id"];
|
|
1405
|
-
if (typeof header === "string" && header.length > 0) return header;
|
|
1406
|
-
if (isJsonObject(body)) {
|
|
1407
|
-
const inBody = body["request_id"];
|
|
1408
|
-
if (typeof inBody === "string" && inBody.length > 0) return inBody;
|
|
1409
|
-
}
|
|
1410
|
-
return null;
|
|
1411
|
-
}
|
|
1412
|
-
function errorCodeOf(body) {
|
|
1413
|
-
if (!isJsonObject(body)) return { code: null, wireMessage: "" };
|
|
1414
|
-
const direct = body["code"];
|
|
1415
|
-
const aliased = body["error"];
|
|
1416
|
-
const message = body["message"];
|
|
1417
|
-
if (typeof direct === "string") {
|
|
1418
|
-
return { code: direct, wireMessage: typeof message === "string" ? message : "" };
|
|
1419
|
-
}
|
|
1420
|
-
if (typeof aliased === "string") {
|
|
1421
|
-
return { code: aliased, wireMessage: typeof message === "string" ? message : "" };
|
|
1422
|
-
}
|
|
1423
|
-
const detail = body["detail"];
|
|
1424
|
-
if (isJsonObject(detail)) {
|
|
1425
|
-
const nested = detail["error"] ?? detail["code"];
|
|
1426
|
-
const nestedMessage = detail["message"];
|
|
1427
|
-
if (typeof nested === "string") {
|
|
1428
|
-
return { code: nested, wireMessage: typeof nestedMessage === "string" ? nestedMessage : "" };
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
if (typeof detail === "string") return { code: null, wireMessage: detail };
|
|
1432
|
-
return { code: null, wireMessage: typeof message === "string" ? message : "" };
|
|
1433
|
-
}
|
|
1434
|
-
function httpFailure(base, wireMessage) {
|
|
1435
|
-
const failure = { ...base };
|
|
1436
|
-
Object.defineProperty(failure, "wireMessage", {
|
|
1437
|
-
value: wireMessage,
|
|
1438
|
-
enumerable: false,
|
|
1439
|
-
writable: false,
|
|
1440
|
-
configurable: false
|
|
1441
|
-
});
|
|
1442
|
-
return Object.freeze(failure);
|
|
1443
|
-
}
|
|
1444
|
-
function createApiClient(options) {
|
|
1445
|
-
const registry = options.registry ?? EMPTY_REGISTRY;
|
|
1446
|
-
const timeoutMs = clampTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
1447
|
-
const retryDelayMs = options.retryDelayMs ?? 400;
|
|
1448
|
-
const idempotentAttempts = Math.max(1, options.maxAttempts ?? 2);
|
|
1449
|
-
const correlationId = typeof options.requestId === "string" && isCorrelatableRequestId(options.requestId) ? options.requestId : null;
|
|
1450
|
-
function buildRequest(spec) {
|
|
1451
|
-
const headers = {
|
|
1452
|
-
accept: "application/json",
|
|
1453
|
-
...spec.headers ?? {}
|
|
1454
|
-
};
|
|
1455
|
-
if (spec.body !== void 0) headers["content-type"] = "application/json";
|
|
1456
|
-
if (correlationId !== null) headers["x-request-id"] = correlationId;
|
|
1457
|
-
for (const [name, credential] of Object.entries(spec.credentials ?? {})) {
|
|
1458
|
-
headers[name] = credential.reveal();
|
|
1459
|
-
}
|
|
1460
|
-
return Object.freeze({
|
|
1461
|
-
method: spec.method,
|
|
1462
|
-
url: apiUrl(options.endpoint, spec.path),
|
|
1463
|
-
headers: Object.freeze(headers),
|
|
1464
|
-
body: spec.body === void 0 ? null : JSON.stringify(spec.body),
|
|
1465
|
-
timeoutMs
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
async function request(spec) {
|
|
1469
|
-
const allowed = isIdempotent(spec.method) ? idempotentAttempts : 1;
|
|
1470
|
-
let attempt = 0;
|
|
1471
|
-
let last = null;
|
|
1472
|
-
while (attempt < allowed) {
|
|
1473
|
-
attempt += 1;
|
|
1474
|
-
const outcome = await attemptOnce(spec, attempt);
|
|
1475
|
-
if (outcome.ok) return outcome;
|
|
1476
|
-
last = outcome;
|
|
1477
|
-
const mayRetry = attempt < allowed && (outcome.kind === "network" || outcome.kind === "timeout" || outcome.kind === "http" && outcome.retryable);
|
|
1478
|
-
if (!mayRetry) return outcome;
|
|
1479
|
-
await options.clock.sleep(retryDelayMs);
|
|
1480
|
-
}
|
|
1481
|
-
return last ?? failure(spec, 1, "exhausted");
|
|
1482
|
-
}
|
|
1483
|
-
function failure(spec, attempts, detail) {
|
|
1484
|
-
return { ok: false, kind: "network", method: spec.method, path: spec.path, detail, attempts, requestId: null };
|
|
1485
|
-
}
|
|
1486
|
-
async function attemptOnce(spec, attempt) {
|
|
1487
|
-
let response;
|
|
1488
|
-
try {
|
|
1489
|
-
response = await options.transport.send(buildRequest(spec));
|
|
1490
|
-
} catch (cause) {
|
|
1491
|
-
if (cause instanceof TransportError && cause.kind === "timeout") {
|
|
1492
|
-
return {
|
|
1493
|
-
ok: false,
|
|
1494
|
-
kind: "timeout",
|
|
1495
|
-
method: spec.method,
|
|
1496
|
-
path: spec.path,
|
|
1497
|
-
timeoutMs,
|
|
1498
|
-
attempts: attempt,
|
|
1499
|
-
requestId: null
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
return failure(spec, attempt, cause instanceof Error ? cause.message : String(cause));
|
|
1503
|
-
}
|
|
1504
|
-
let parsed;
|
|
1505
|
-
let parseDetail = null;
|
|
1506
|
-
if (response.body.trim().length === 0) {
|
|
1507
|
-
parsed = null;
|
|
1508
|
-
} else {
|
|
1509
|
-
try {
|
|
1510
|
-
parsed = JSON.parse(response.body);
|
|
1511
|
-
} catch (cause) {
|
|
1512
|
-
parseDetail = cause instanceof Error ? cause.message : "response body is not JSON";
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
const requestId = requestIdOf(response.headers, parsed);
|
|
1516
|
-
const accepted = response.status >= 200 && response.status < 300 || (spec.acceptStatuses ?? []).includes(response.status);
|
|
1517
|
-
if (accepted) {
|
|
1518
|
-
if (parseDetail !== null) {
|
|
1519
|
-
return {
|
|
1520
|
-
ok: false,
|
|
1521
|
-
kind: "malformed",
|
|
1522
|
-
method: spec.method,
|
|
1523
|
-
path: spec.path,
|
|
1524
|
-
status: response.status,
|
|
1525
|
-
detail: parseDetail,
|
|
1526
|
-
requestId,
|
|
1527
|
-
attempts: attempt
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
return {
|
|
1531
|
-
ok: true,
|
|
1532
|
-
value: parsed ?? null,
|
|
1533
|
-
status: response.status,
|
|
1534
|
-
requestId,
|
|
1535
|
-
attempts: attempt
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
const { code, wireMessage } = errorCodeOf(parsed);
|
|
1539
|
-
const entry = code === null ? null : registry.lookup(code);
|
|
1540
|
-
return httpFailure(
|
|
1541
|
-
{
|
|
1542
|
-
ok: false,
|
|
1543
|
-
kind: "http",
|
|
1544
|
-
method: spec.method,
|
|
1545
|
-
path: spec.path,
|
|
1546
|
-
status: response.status,
|
|
1547
|
-
code,
|
|
1548
|
-
entry,
|
|
1549
|
-
retryable: entry?.retryable === true || RETRYABLE_STATUSES.includes(response.status),
|
|
1550
|
-
requestId,
|
|
1551
|
-
attempts: attempt
|
|
1552
|
-
},
|
|
1553
|
-
wireMessage
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
return Object.freeze({ request, baseUrl: options.endpoint.baseUrl });
|
|
1557
|
-
}
|
|
1558
|
-
function describeFailure(failure) {
|
|
1559
|
-
const where = `${failure.method} ${failure.path}`;
|
|
1560
|
-
switch (failure.kind) {
|
|
1561
|
-
case "network":
|
|
1562
|
-
return `${where} could not be reached: ${failure.detail}`;
|
|
1563
|
-
case "timeout":
|
|
1564
|
-
return `${where} did not answer within ${failure.timeoutMs}ms`;
|
|
1565
|
-
case "malformed":
|
|
1566
|
-
return `${where} answered ${failure.status} with a body that is not JSON: ${failure.detail}`;
|
|
1567
|
-
case "http": {
|
|
1568
|
-
const summary = `${where} answered ${failure.status} ${failure.code ?? "(no code)"}`;
|
|
1569
|
-
return failure.entry === null ? summary : `${summary}: ${failure.entry.display}`;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
function failureDetails(failure) {
|
|
1574
|
-
const lines = [];
|
|
1575
|
-
if (failure.requestId !== null) lines.push(`request id: ${failure.requestId}`);
|
|
1576
|
-
if (failure.attempts > 1) lines.push(`attempts: ${failure.attempts}`);
|
|
1577
|
-
if (failure.kind === "http") {
|
|
1578
|
-
lines.push(`retryable: ${failure.retryable ? "yes" : "no"}`);
|
|
1579
|
-
if (failure.entry !== null) {
|
|
1580
|
-
lines.push(`cause: ${failure.entry.note}`);
|
|
1581
|
-
lines.push(`docs: ${failure.entry.docUrl}`);
|
|
1582
|
-
} else if (failure.code !== null) {
|
|
1583
|
-
lines.push(`This code is not in the error registry this run loaded.`);
|
|
1584
|
-
lines.push(`Run \`everfur explain-error ${failure.code}\` against a contract-public projection.`);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
return Object.freeze(lines);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// contract/contract-public.json
|
|
1591
|
-
var contract_public_default = {
|
|
1592
|
-
digest: {
|
|
1593
|
-
},
|
|
1594
|
-
generatedAt: "2026-08-30T07:05:23Z",
|
|
1595
|
-
projection: {
|
|
1596
|
-
"action-classes.yaml": {
|
|
1597
|
-
action_classes: [
|
|
1598
|
-
{
|
|
1599
|
-
default_exposure: "agent_allowed",
|
|
1600
|
-
description: "Reads only material that is the same for every caller: the contract itself, an error registry, a capability list. No tenant data is touched and no state changes.",
|
|
1601
|
-
id: "A0",
|
|
1602
|
-
reaches_tenant_data: false,
|
|
1603
|
-
reversible: true,
|
|
1604
|
-
title: "Inert read"
|
|
1605
|
-
},
|
|
1606
|
-
{
|
|
1607
|
-
default_exposure: "agent_with_confirmation",
|
|
1608
|
-
description: "Creates or updates tenant state that the owner can inspect and undo: a pet profile, a consent row, a records request, a message turn. The write is visible to the owner and has a defined reversal.",
|
|
1609
|
-
id: "A3",
|
|
1610
|
-
reaches_tenant_data: true,
|
|
1611
|
-
reversible: true,
|
|
1612
|
-
title: "Reversible write"
|
|
1613
|
-
},
|
|
1614
|
-
{
|
|
1615
|
-
default_exposure: "agent_with_confirmation",
|
|
1616
|
-
description: "Reads tenant-scoped material that is not health content: a pet profile, a request status, a list of conversation titles. Wrong-caller risk is a metadata disclosure, not a health disclosure.",
|
|
1617
|
-
id: "A1",
|
|
1618
|
-
reaches_tenant_data: true,
|
|
1619
|
-
reversible: true,
|
|
1620
|
-
title: "Scoped read"
|
|
1621
|
-
},
|
|
1622
|
-
{
|
|
1623
|
-
default_exposure: "never_agent",
|
|
1624
|
-
description: "Changes the machinery that decides who may do any of the above: minting or revoking a credential, granting or denying a capability, flipping a runtime gate. A wrong invocation here is not one bad request, it is a changed posture for every subsequent request.",
|
|
1625
|
-
id: "A5",
|
|
1626
|
-
reaches_tenant_data: true,
|
|
1627
|
-
reversible: false,
|
|
1628
|
-
title: "Control-plane change"
|
|
1629
|
-
},
|
|
1630
|
-
{
|
|
1631
|
-
default_exposure: "never_agent",
|
|
1632
|
-
description: "Produces an effect that cannot be taken back: content handed to a third party, an export written to a downloadable artifact, a share link, an outbound send. Reversal changes future access only, never the copy the recipient already holds.",
|
|
1633
|
-
id: "A4",
|
|
1634
|
-
reaches_tenant_data: true,
|
|
1635
|
-
reversible: false,
|
|
1636
|
-
title: "Unrecallable effect"
|
|
1637
|
-
},
|
|
1638
|
-
{
|
|
1639
|
-
default_exposure: "never_agent",
|
|
1640
|
-
description: "Reads restricted content: a medical record projection, a conversation transcript, a classification derived from either. A read cannot be undone once it has been served, so this class carries the never_agent default even though nothing is written.",
|
|
1641
|
-
id: "A2",
|
|
1642
|
-
reaches_tenant_data: true,
|
|
1643
|
-
reversible: true,
|
|
1644
|
-
title: "Restricted-content read"
|
|
1645
|
-
}
|
|
1646
|
-
],
|
|
1647
|
-
exposures: [
|
|
1648
|
-
{
|
|
1649
|
-
description: "An agent may invoke this without a human in the loop. Reserved for calls that read nothing tenant-scoped and change nothing.",
|
|
1650
|
-
id: "agent_allowed",
|
|
1651
|
-
rank: 0
|
|
1652
|
-
},
|
|
1653
|
-
{
|
|
1654
|
-
description: "An agent may prepare the call, but a human must approve the specific invocation before it is sent. The confirmation must name the concrete effect, not the capability in the abstract.",
|
|
1655
|
-
id: "agent_with_confirmation",
|
|
1656
|
-
rank: 1
|
|
1657
|
-
},
|
|
1658
|
-
{
|
|
1659
|
-
description: "No agent path exists. The call is reachable only from a first-party surface driven by the person the data belongs to. A never_agent action publishes its id, its exposure, and its class, and nothing else.",
|
|
1660
|
-
id: "never_agent",
|
|
1661
|
-
rank: 2
|
|
1662
|
-
}
|
|
1663
|
-
],
|
|
1664
|
-
schema_version: 1
|
|
1665
|
-
},
|
|
1666
|
-
"capabilities.yaml": {
|
|
1667
|
-
packs: [
|
|
1668
|
-
{
|
|
1669
|
-
actions: [
|
|
1670
|
-
{
|
|
1671
|
-
action_class: "A1",
|
|
1672
|
-
detail: "Polls the status of an asynchronous analysis job. Returns job state; the result payload it eventually carries is the analysis output, not the source media.",
|
|
1673
|
-
exposure: "agent_with_confirmation",
|
|
1674
|
-
id: "poll_analysis_job",
|
|
1675
|
-
route: "GET /api/v1/analysis/jobs/{job_id}"
|
|
1676
|
-
},
|
|
1677
|
-
{
|
|
1678
|
-
action_class: "A3",
|
|
1679
|
-
exposure: "never_agent",
|
|
1680
|
-
id: "submit_gait_analysis"
|
|
1681
|
-
}
|
|
1682
|
-
],
|
|
1683
|
-
data_class: "confidential",
|
|
1684
|
-
entitlement: {
|
|
1685
|
-
key: "video.gait.execute",
|
|
1686
|
-
risk_tier: "standard"
|
|
1687
|
-
},
|
|
1688
|
-
id: "video.gait.execute",
|
|
1689
|
-
idempotent_operations: [],
|
|
1690
|
-
required_disclaimer: {
|
|
1691
|
-
confirmed_on_wire: false,
|
|
1692
|
-
id: "none",
|
|
1693
|
-
note: "No disclaimer copy is attached to the analysis surface in code. The analysis response carries interpretation text but no separate product-owned disclaimer field.",
|
|
1694
|
-
text: null
|
|
1695
|
-
},
|
|
1696
|
-
runtime_gate: {
|
|
1697
|
-
fail_closed: true,
|
|
1698
|
-
kind: "ddb_flag"
|
|
1699
|
-
},
|
|
1700
|
-
support: "support@everfur.com",
|
|
1701
|
-
surface: [
|
|
1702
|
-
"consumer_app"
|
|
1703
|
-
],
|
|
1704
|
-
title: "Run gait and mobility analysis on an uploaded video",
|
|
1705
|
-
version: "1.0.0"
|
|
1706
|
-
},
|
|
1707
|
-
{
|
|
1708
|
-
actions: [
|
|
1709
|
-
{
|
|
1710
|
-
action_class: "A1",
|
|
1711
|
-
detail: "Returns conversation containers with titles and cursors for the resolved user_ref. Titles are user-authored and may carry content, so this is scoped metadata rather than an inert read.",
|
|
1712
|
-
exposure: "agent_with_confirmation",
|
|
1713
|
-
id: "list_conversations",
|
|
1714
|
-
route: "GET /api/v1/widget/v1/conversations"
|
|
1715
|
-
},
|
|
1716
|
-
{
|
|
1717
|
-
action_class: "A2",
|
|
1718
|
-
exposure: "never_agent",
|
|
1719
|
-
id: "get_messages"
|
|
1720
|
-
}
|
|
1721
|
-
],
|
|
1722
|
-
data_class: "restricted",
|
|
1723
|
-
entitlement: {
|
|
1724
|
-
key: "chat.conversation.read",
|
|
1725
|
-
risk_tier: "standard"
|
|
1726
|
-
},
|
|
1727
|
-
id: "chat.conversation.read",
|
|
1728
|
-
idempotent_operations: [],
|
|
1729
|
-
required_disclaimer: {
|
|
1730
|
-
confirmed_on_wire: false,
|
|
1731
|
-
id: "entitlement_verdict_disclaimer",
|
|
1732
|
-
note: "No chat-surface disclaimer copy exists in code. See chat.message.create.",
|
|
1733
|
-
text: null
|
|
1734
|
-
},
|
|
1735
|
-
runtime_gate: {
|
|
1736
|
-
fail_closed: true,
|
|
1737
|
-
key: "chat_enabled",
|
|
1738
|
-
kind: "client_feature"
|
|
1739
|
-
},
|
|
1740
|
-
support: "support@everfur.com",
|
|
1741
|
-
surface: [
|
|
1742
|
-
"partner_widget"
|
|
1743
|
-
],
|
|
1744
|
-
title: "Read widget conversations and their transcripts",
|
|
1745
|
-
version: "1.0.0"
|
|
1746
|
-
},
|
|
1747
|
-
{
|
|
1748
|
-
actions: [
|
|
1749
|
-
{
|
|
1750
|
-
action_class: "A1",
|
|
1751
|
-
detail: "Returns one request's status. A request whose pet is not one of this caller's widget pets is reported as not found.",
|
|
1752
|
-
exposure: "agent_with_confirmation",
|
|
1753
|
-
id: "get_request",
|
|
1754
|
-
route: "GET /api/v1/widget/v1/records/requests/{request_id}"
|
|
1755
|
-
},
|
|
1756
|
-
{
|
|
1757
|
-
action_class: "A1",
|
|
1758
|
-
detail: "Returns request status rows for the caller's own widget pets. Status and timestamps only; never document content.",
|
|
1759
|
-
exposure: "agent_with_confirmation",
|
|
1760
|
-
id: "list_requests",
|
|
1761
|
-
route: "GET /api/v1/widget/v1/records/requests"
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
action_class: "A1",
|
|
1765
|
-
detail: "Returns whether consent is granted, at which copy version, and when. Consent metadata, not record content.",
|
|
1766
|
-
exposure: "agent_with_confirmation",
|
|
1767
|
-
id: "get_consent_state",
|
|
1768
|
-
route: "GET /api/v1/widget/v1/records/pets/{pet_ref}/consent"
|
|
1769
|
-
},
|
|
1770
|
-
{
|
|
1771
|
-
action_class: "A2",
|
|
1772
|
-
exposure: "never_agent",
|
|
1773
|
-
id: "get_record"
|
|
1774
|
-
}
|
|
1775
|
-
],
|
|
1776
|
-
data_class: "restricted",
|
|
1777
|
-
entitlement: {
|
|
1778
|
-
key: "records.record.read",
|
|
1779
|
-
risk_tier: "standard"
|
|
1780
|
-
},
|
|
1781
|
-
id: "records.record.read",
|
|
1782
|
-
idempotent_operations: [],
|
|
1783
|
-
required_disclaimer: {
|
|
1784
|
-
confirmed_on_wire: true,
|
|
1785
|
-
id: "records_informational",
|
|
1786
|
-
note: "Emitted on both the granted and the withheld projection, so the disclaimer is present even when the content is not.",
|
|
1787
|
-
text: "Everfur summarizes what your vet recorded. It is informational, not diagnostic."
|
|
1788
|
-
},
|
|
1789
|
-
runtime_gate: {
|
|
1790
|
-
fail_closed: true,
|
|
1791
|
-
kind: "ddb_flag"
|
|
1792
|
-
},
|
|
1793
|
-
support: "support@everfur.com",
|
|
1794
|
-
surface: [
|
|
1795
|
-
"partner_widget"
|
|
1796
|
-
],
|
|
1797
|
-
title: "Read the redacted records projection for a pet",
|
|
1798
|
-
version: "1.0.0"
|
|
1799
|
-
},
|
|
1800
|
-
{
|
|
1801
|
-
actions: [
|
|
1802
|
-
{
|
|
1803
|
-
action_class: "A1",
|
|
1804
|
-
detail: "Returns the reduced presentation state for the pet, including which breed art clip to play. Derived presentation data keyed to a pet the caller owns; it carries no health findings.",
|
|
1805
|
-
exposure: "agent_with_confirmation",
|
|
1806
|
-
id: "get_pet_state",
|
|
1807
|
-
route: "GET /api/v1/me/pets/{pet_id}/state"
|
|
1808
|
-
}
|
|
1809
|
-
],
|
|
1810
|
-
data_class: "internal",
|
|
1811
|
-
entitlement: {
|
|
1812
|
-
key: "animations.state.read",
|
|
1813
|
-
risk_tier: "standard"
|
|
1814
|
-
},
|
|
1815
|
-
id: "animations.state.read",
|
|
1816
|
-
idempotent_operations: [],
|
|
1817
|
-
required_disclaimer: {
|
|
1818
|
-
confirmed_on_wire: false,
|
|
1819
|
-
id: "none",
|
|
1820
|
-
note: "A presentation surface with no health claim, so no disclaimer applies.",
|
|
1821
|
-
text: null
|
|
1822
|
-
},
|
|
1823
|
-
runtime_gate: {
|
|
1824
|
-
fail_closed: true,
|
|
1825
|
-
kind: "ddb_flag"
|
|
1826
|
-
},
|
|
1827
|
-
support: "support@everfur.com",
|
|
1828
|
-
surface: [
|
|
1829
|
-
"consumer_app"
|
|
1830
|
-
],
|
|
1831
|
-
title: "Read the animated pet-state for a pet",
|
|
1832
|
-
version: "1.0.0"
|
|
1833
|
-
},
|
|
1834
|
-
{
|
|
1835
|
-
actions: [
|
|
1836
|
-
{
|
|
1837
|
-
action_class: "A1",
|
|
1838
|
-
detail: "Scores an image for usability before analysis. Returns a quality verdict, not a health finding.",
|
|
1839
|
-
exposure: "agent_with_confirmation",
|
|
1840
|
-
id: "precheck_image",
|
|
1841
|
-
route: "POST /api/v1/analysis/image/precheck"
|
|
1842
|
-
},
|
|
1843
|
-
{
|
|
1844
|
-
action_class: "A3",
|
|
1845
|
-
exposure: "never_agent",
|
|
1846
|
-
id: "analyze_image"
|
|
1847
|
-
},
|
|
1848
|
-
{
|
|
1849
|
-
action_class: "A3",
|
|
1850
|
-
exposure: "never_agent",
|
|
1851
|
-
id: "analyze_image_from_s3"
|
|
1852
|
-
}
|
|
1853
|
-
],
|
|
1854
|
-
data_class: "confidential",
|
|
1855
|
-
entitlement: {
|
|
1856
|
-
key: "photo.checkup.execute",
|
|
1857
|
-
risk_tier: "standard"
|
|
1858
|
-
},
|
|
1859
|
-
id: "photo.checkup.execute",
|
|
1860
|
-
idempotent_operations: [
|
|
1861
|
-
{
|
|
1862
|
-
key: "Idempotency-Key header",
|
|
1863
|
-
operation: "analyze_image",
|
|
1864
|
-
semantics: "Explicit claim, run, complete. A replay of a completed key returns the recorded response; an in-flight key returns 409 IDEMPOTENCY_KEY_IN_FLIGHT; the same key with a different request body returns 409 IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST."
|
|
1865
|
-
}
|
|
1866
|
-
],
|
|
1867
|
-
required_disclaimer: {
|
|
1868
|
-
confirmed_on_wire: false,
|
|
1869
|
-
id: "none",
|
|
1870
|
-
note: "As with video.gait.execute, no product-owned disclaimer field is attached to the analysis response.",
|
|
1871
|
-
text: null
|
|
1872
|
-
},
|
|
1873
|
-
runtime_gate: {
|
|
1874
|
-
fail_closed: false,
|
|
1875
|
-
key: null,
|
|
1876
|
-
kind: "none"
|
|
1877
|
-
},
|
|
1878
|
-
support: "support@everfur.com",
|
|
1879
|
-
surface: [
|
|
1880
|
-
"consumer_app",
|
|
1881
|
-
"partner_widget"
|
|
1882
|
-
],
|
|
1883
|
-
title: "Run a photo checkup on a pet image",
|
|
1884
|
-
version: "1.0.0"
|
|
1885
|
-
},
|
|
1886
|
-
{
|
|
1887
|
-
actions: [
|
|
1888
|
-
{
|
|
1889
|
-
action_class: "A2",
|
|
1890
|
-
exposure: "never_agent",
|
|
1891
|
-
id: "classify_urgency"
|
|
1892
|
-
},
|
|
1893
|
-
{
|
|
1894
|
-
action_class: "A3",
|
|
1895
|
-
detail: "Opens an empty conversation container bound to the resolved (client_id, user_ref). Carries no content of its own.",
|
|
1896
|
-
exposure: "agent_with_confirmation",
|
|
1897
|
-
id: "create_conversation",
|
|
1898
|
-
route: "POST /api/v1/widget/v1/conversations"
|
|
1899
|
-
},
|
|
1900
|
-
{
|
|
1901
|
-
action_class: "A3",
|
|
1902
|
-
exposure: "never_agent",
|
|
1903
|
-
id: "analyze_in_chat"
|
|
1904
|
-
},
|
|
1905
|
-
{
|
|
1906
|
-
action_class: "A3",
|
|
1907
|
-
exposure: "never_agent",
|
|
1908
|
-
id: "send_message"
|
|
1909
|
-
}
|
|
1910
|
-
],
|
|
1911
|
-
data_class: "restricted",
|
|
1912
|
-
entitlement: {
|
|
1913
|
-
key: "chat.message.create",
|
|
1914
|
-
risk_tier: "standard"
|
|
1915
|
-
},
|
|
1916
|
-
id: "chat.message.create",
|
|
1917
|
-
idempotent_operations: [],
|
|
1918
|
-
required_disclaimer: {
|
|
1919
|
-
confirmed_on_wire: false,
|
|
1920
|
-
id: "entitlement_verdict_disclaimer",
|
|
1921
|
-
note: "The entitlement verdict carries a product-owned disclaimer block, but its text and version both default to the empty string and no code path sets them. No chat-surface copy exists to quote.",
|
|
1922
|
-
text: null
|
|
1923
|
-
},
|
|
1924
|
-
runtime_gate: {
|
|
1925
|
-
fail_closed: true,
|
|
1926
|
-
key: "chat_enabled",
|
|
1927
|
-
kind: "client_feature"
|
|
1928
|
-
},
|
|
1929
|
-
support: "support@everfur.com",
|
|
1930
|
-
surface: [
|
|
1931
|
-
"partner_widget"
|
|
1932
|
-
],
|
|
1933
|
-
title: "Send a message to the AI vet chat",
|
|
1934
|
-
version: "1.0.0"
|
|
1935
|
-
},
|
|
1936
|
-
{
|
|
1937
|
-
actions: [
|
|
1938
|
-
{
|
|
1939
|
-
action_class: "A3",
|
|
1940
|
-
detail: "Creates an owner-provided records request against a widget pet and materializes the consumer_pets shadow row the records FK needs. Carries no document content yet.",
|
|
1941
|
-
exposure: "agent_with_confirmation",
|
|
1942
|
-
id: "create_records_request",
|
|
1943
|
-
route: "POST /api/v1/widget/v1/records/requests"
|
|
1944
|
-
},
|
|
1945
|
-
{
|
|
1946
|
-
action_class: "A3",
|
|
1947
|
-
exposure: "never_agent",
|
|
1948
|
-
id: "complete_upload"
|
|
1949
|
-
},
|
|
1950
|
-
{
|
|
1951
|
-
action_class: "A3",
|
|
1952
|
-
exposure: "never_agent",
|
|
1953
|
-
id: "grant_consent"
|
|
1954
|
-
},
|
|
1955
|
-
{
|
|
1956
|
-
action_class: "A3",
|
|
1957
|
-
exposure: "never_agent",
|
|
1958
|
-
id: "initiate_upload"
|
|
1959
|
-
},
|
|
1960
|
-
{
|
|
1961
|
-
action_class: "A3",
|
|
1962
|
-
exposure: "never_agent",
|
|
1963
|
-
id: "revoke_consent"
|
|
1964
|
-
}
|
|
1965
|
-
],
|
|
1966
|
-
data_class: "restricted",
|
|
1967
|
-
entitlement: {
|
|
1968
|
-
key: "records.upload.create",
|
|
1969
|
-
risk_tier: "standard"
|
|
1970
|
-
},
|
|
1971
|
-
id: "records.upload.create",
|
|
1972
|
-
idempotent_operations: [
|
|
1973
|
-
{
|
|
1974
|
-
key: "body.idempotency_key, scoped to the resolved user_id",
|
|
1975
|
-
operation: "create_records_request",
|
|
1976
|
-
semantics: "Partial unique index on (user_id, idempotency_key). A replay returns the existing row instead of inserting a second empty request."
|
|
1977
|
-
},
|
|
1978
|
-
{
|
|
1979
|
-
key: "row grain (pet_id), not the supplied idempotency_key",
|
|
1980
|
-
operation: "grant_consent",
|
|
1981
|
-
semantics: "The consent write is a row-grain UPSERT and is therefore idempotent by construction. The body's idempotency_key is accepted and ignored; do not rely on it as the dedup token."
|
|
1982
|
-
}
|
|
1983
|
-
],
|
|
1984
|
-
required_disclaimer: {
|
|
1985
|
-
confirmed_on_wire: true,
|
|
1986
|
-
id: "records_informational",
|
|
1987
|
-
note: "Sourced from the EverfurRecord field default so the partner surface can never drift from the owner-facing copy.",
|
|
1988
|
-
text: "Everfur summarizes what your vet recorded. It is informational, not diagnostic."
|
|
1989
|
-
},
|
|
1990
|
-
runtime_gate: {
|
|
1991
|
-
fail_closed: true,
|
|
1992
|
-
kind: "ddb_flag"
|
|
1993
|
-
},
|
|
1994
|
-
support: "support@everfur.com",
|
|
1995
|
-
surface: [
|
|
1996
|
-
"partner_widget"
|
|
1997
|
-
],
|
|
1998
|
-
title: "Create a records request and upload source documents onto it",
|
|
1999
|
-
version: "1.0.0"
|
|
2000
|
-
},
|
|
2001
|
-
{
|
|
2002
|
-
actions: [
|
|
2003
|
-
{
|
|
2004
|
-
action_class: "A4",
|
|
2005
|
-
exposure: "never_agent",
|
|
2006
|
-
id: "export_record"
|
|
2007
|
-
}
|
|
2008
|
-
],
|
|
2009
|
-
data_class: "restricted",
|
|
2010
|
-
entitlement: {
|
|
2011
|
-
key: "records.record.export",
|
|
2012
|
-
risk_tier: "high"
|
|
2013
|
-
},
|
|
2014
|
-
id: "records.record.export",
|
|
2015
|
-
idempotent_operations: [],
|
|
2016
|
-
required_disclaimer: {
|
|
2017
|
-
confirmed_on_wire: false,
|
|
2018
|
-
id: "records_informational",
|
|
2019
|
-
note: "The copy is the records disclaimer, but nothing emits it on this capability today because the capability has no route.",
|
|
2020
|
-
text: "Everfur summarizes what your vet recorded. It is informational, not diagnostic."
|
|
2021
|
-
},
|
|
2022
|
-
runtime_gate: {
|
|
2023
|
-
fail_closed: true,
|
|
2024
|
-
key: null,
|
|
2025
|
-
kind: "none"
|
|
2026
|
-
},
|
|
2027
|
-
support: "support@everfur.com",
|
|
2028
|
-
surface: [
|
|
2029
|
-
"unrouted"
|
|
2030
|
-
],
|
|
2031
|
-
title: "Export a pet's records as a downloadable artifact",
|
|
2032
|
-
version: "0.1.0"
|
|
2033
|
-
},
|
|
2034
|
-
{
|
|
2035
|
-
actions: [
|
|
2036
|
-
{
|
|
2037
|
-
action_class: "A4",
|
|
2038
|
-
exposure: "never_agent",
|
|
2039
|
-
id: "share_record"
|
|
2040
|
-
}
|
|
2041
|
-
],
|
|
2042
|
-
data_class: "restricted",
|
|
2043
|
-
entitlement: {
|
|
2044
|
-
key: "records.record.share",
|
|
2045
|
-
risk_tier: "high"
|
|
2046
|
-
},
|
|
2047
|
-
id: "records.record.share",
|
|
2048
|
-
idempotent_operations: [],
|
|
2049
|
-
required_disclaimer: {
|
|
2050
|
-
confirmed_on_wire: false,
|
|
2051
|
-
id: "records_informational",
|
|
2052
|
-
note: "No route, so nothing emits it. See records.record.export.",
|
|
2053
|
-
text: "Everfur summarizes what your vet recorded. It is informational, not diagnostic."
|
|
2054
|
-
},
|
|
2055
|
-
runtime_gate: {
|
|
2056
|
-
fail_closed: true,
|
|
2057
|
-
key: null,
|
|
2058
|
-
kind: "none"
|
|
2059
|
-
},
|
|
2060
|
-
support: "support@everfur.com",
|
|
2061
|
-
surface: [
|
|
2062
|
-
"unrouted"
|
|
2063
|
-
],
|
|
2064
|
-
title: "Share a pet's records with a third party",
|
|
2065
|
-
version: "0.1.0"
|
|
2066
|
-
}
|
|
2067
|
-
],
|
|
2068
|
-
registry_kind: "authoritative",
|
|
2069
|
-
registry_version: "2026.08.22",
|
|
2070
|
-
schema_version: 1
|
|
2071
|
-
},
|
|
2072
|
-
"credentials.yaml": {
|
|
2073
|
-
credentials: [
|
|
2074
|
-
{
|
|
2075
|
-
body_charset: "hex",
|
|
2076
|
-
body_length: 64,
|
|
2077
|
-
description: "The publishable tenant key. Shipped inside partner apps and web pages, so it is public by construction and identifies the tenant rather than authorising a person. It resolves to a client_id and nothing else. It still carries a redaction pattern, because a key in a log is a key someone can enumerate tenants with; see the header for what does and does not consume that pattern today.",
|
|
2078
|
-
hashed_at_rest: false,
|
|
2079
|
-
holder: "partner_client",
|
|
2080
|
-
id: "partner_publishable_key",
|
|
2081
|
-
prefix: "pk_live_",
|
|
2082
|
-
redaction_regex: "pk_live_[A-Za-z0-9]{8,}",
|
|
2083
|
-
rotatable: true,
|
|
2084
|
-
secret: false,
|
|
2085
|
-
total_length: 72
|
|
2086
|
-
},
|
|
2087
|
-
{
|
|
2088
|
-
body_charset: "hex",
|
|
2089
|
-
body_length: null,
|
|
2090
|
-
description: "RESERVED. A test-mode publishable prefix, registered so that a redactor would scrub it and the validator rejects it in a production credential slot before any such key is ever minted.",
|
|
2091
|
-
hashed_at_rest: false,
|
|
2092
|
-
holder: "partner_client",
|
|
2093
|
-
id: "partner_publishable_key_test",
|
|
2094
|
-
prefix: "pk_test_",
|
|
2095
|
-
redaction_regex: "pk_test_[A-Za-z0-9]{8,}",
|
|
2096
|
-
rotatable: true,
|
|
2097
|
-
secret: false,
|
|
2098
|
-
total_length: null
|
|
2099
|
-
},
|
|
2100
|
-
{
|
|
2101
|
-
body_charset: "jwt",
|
|
2102
|
-
body_length: null,
|
|
2103
|
-
description: "The short-lived HS256 bearer a device carries after the mint. It has no literal prefix, so the redaction pattern matches the JWS compact serialisation instead. Not stored server-side at all, which is why it cannot be revoked directly; revocation works by deactivating the signing key_id it carries, which is re-checked live on every request. Publishable as a concept because every integrating partner must know it exists, but the token value itself is a secret.",
|
|
2104
|
-
hashed_at_rest: false,
|
|
2105
|
-
holder: "partner_device",
|
|
2106
|
-
id: "partner_session_bearer",
|
|
2107
|
-
prefix: null,
|
|
2108
|
-
redaction_regex: "eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}(\\.[A-Za-z0-9_-]+)?",
|
|
2109
|
-
rotatable: false,
|
|
2110
|
-
secret: true,
|
|
2111
|
-
total_length: null
|
|
2112
|
-
}
|
|
2113
|
-
],
|
|
2114
|
-
schema_version: 1
|
|
2115
|
-
},
|
|
2116
|
-
"decision-trees.yaml": {
|
|
2117
|
-
schema_version: 1,
|
|
2118
|
-
trees: [
|
|
2119
|
-
{
|
|
2120
|
-
decides: "The host project's shape, and the verb that installs a package into it.",
|
|
2121
|
-
entry_question_id: "project-shape",
|
|
2122
|
-
principles: [
|
|
2123
|
-
{
|
|
2124
|
-
doc_path: null,
|
|
2125
|
-
id: "duplicate-react-native-is-silent",
|
|
2126
|
-
statement: "Two copies of react-native in one dependency tree do not raise. The app builds, the bundle serves, and the screen comes up empty, because the renderer that mounted the component is not the renderer that owns the host view registry. Nothing in the stack reports it. If a surface that should show a component shows nothing at all, check for a second react-native before you debug the component, and hoist to a single copy rather than deduping by hand at the bundler.",
|
|
2127
|
-
title: "A duplicate react-native renders a blank screen, not an error"
|
|
2128
|
-
},
|
|
2129
|
-
{
|
|
2130
|
-
doc_path: null,
|
|
2131
|
-
id: "expo-go-cannot-load-native",
|
|
2132
|
-
statement: "The optional peer dependencies some capabilities bind ship native code. Expo Go carries a fixed native runtime, so a managed project that mounts one of those capabilities needs a development build before the capability can run at all. Installing the peer and reloading Expo Go is the failure that looks like an SDK bug.",
|
|
2133
|
-
title: "Expo Go cannot load a native module, whatever the plan says"
|
|
2134
|
-
}
|
|
2135
|
-
],
|
|
2136
|
-
questions: [
|
|
2137
|
-
{
|
|
2138
|
-
detectable: true,
|
|
2139
|
-
id: "node-package-manager",
|
|
2140
|
-
options: [
|
|
2141
|
-
{
|
|
2142
|
-
id: "bun",
|
|
2143
|
-
label: "Bun",
|
|
2144
|
-
outcome: {
|
|
2145
|
-
attributes: {
|
|
2146
|
-
install_verb: "bun add",
|
|
2147
|
-
package_manager_applies: true
|
|
2148
|
-
},
|
|
2149
|
-
doc_paths: [],
|
|
2150
|
-
kind: "terminal"
|
|
2151
|
-
},
|
|
2152
|
-
when: "bun.lockb is the only lockfile, or package.json names bun in packageManager."
|
|
2153
|
-
},
|
|
2154
|
-
{
|
|
2155
|
-
id: "npm",
|
|
2156
|
-
label: "npm",
|
|
2157
|
-
outcome: {
|
|
2158
|
-
attributes: {
|
|
2159
|
-
install_verb: "npm install",
|
|
2160
|
-
package_manager_applies: true
|
|
2161
|
-
},
|
|
2162
|
-
doc_paths: [],
|
|
2163
|
-
kind: "terminal"
|
|
2164
|
-
},
|
|
2165
|
-
when: "package-lock.json is the only lockfile, or package.json names npm in packageManager."
|
|
2166
|
-
},
|
|
2167
|
-
{
|
|
2168
|
-
id: "pnpm",
|
|
2169
|
-
label: "pnpm",
|
|
2170
|
-
outcome: {
|
|
2171
|
-
attributes: {
|
|
2172
|
-
install_verb: "pnpm add",
|
|
2173
|
-
package_manager_applies: true
|
|
2174
|
-
},
|
|
2175
|
-
doc_paths: [],
|
|
2176
|
-
kind: "terminal"
|
|
2177
|
-
},
|
|
2178
|
-
when: "pnpm-lock.yaml is the only lockfile, or package.json names pnpm in packageManager."
|
|
2179
|
-
},
|
|
2180
|
-
{
|
|
2181
|
-
id: "yarn",
|
|
2182
|
-
label: "Yarn",
|
|
2183
|
-
outcome: {
|
|
2184
|
-
attributes: {
|
|
2185
|
-
install_verb: "yarn add",
|
|
2186
|
-
package_manager_applies: true
|
|
2187
|
-
},
|
|
2188
|
-
doc_paths: [],
|
|
2189
|
-
kind: "terminal"
|
|
2190
|
-
},
|
|
2191
|
-
when: "yarn.lock is the only lockfile, or package.json names yarn in packageManager."
|
|
2192
|
-
}
|
|
2193
|
-
],
|
|
2194
|
-
prompt: "Which package manager installs into this project?",
|
|
2195
|
-
supply_with: 'add a "packageManager" field to package.json, or leave exactly one lockfile in the project root',
|
|
2196
|
-
why_it_is_asked: "the package manager could not be identified, and the wrong install verb writes the wrong lockfile, so the next person to install gets a dependency tree nobody chose"
|
|
2197
|
-
},
|
|
2198
|
-
{
|
|
2199
|
-
detectable: true,
|
|
2200
|
-
id: "project-shape",
|
|
2201
|
-
options: [
|
|
2202
|
-
{
|
|
2203
|
-
id: "bare-react-native",
|
|
2204
|
-
label: "Bare React Native app",
|
|
2205
|
-
outcome: {
|
|
2206
|
-
kind: "branch",
|
|
2207
|
-
next_question_id: "node-package-manager"
|
|
2208
|
-
},
|
|
2209
|
-
when: "package.json declares react-native and the native projects are committed."
|
|
2210
|
-
},
|
|
2211
|
-
{
|
|
2212
|
-
id: "expo-managed",
|
|
2213
|
-
label: "Expo managed React Native app",
|
|
2214
|
-
outcome: {
|
|
2215
|
-
kind: "branch",
|
|
2216
|
-
next_question_id: "node-package-manager"
|
|
2217
|
-
},
|
|
2218
|
-
when: "package.json declares expo and the project has no committed ios/ or android/ directory."
|
|
2219
|
-
},
|
|
2220
|
-
{
|
|
2221
|
-
id: "flutter",
|
|
2222
|
-
label: "Flutter application",
|
|
2223
|
-
outcome: {
|
|
2224
|
-
attributes: {
|
|
2225
|
-
import_target: "package:everfur_sdk/everfur_sdk.dart",
|
|
2226
|
-
install_commands: [
|
|
2227
|
-
"flutter pub add everfur_sdk",
|
|
2228
|
-
"flutter pub get"
|
|
2229
|
-
],
|
|
2230
|
-
install_style: "pub",
|
|
2231
|
-
manifest: "pubspec.yaml",
|
|
2232
|
-
package: "everfur_sdk",
|
|
2233
|
-
package_manager_applies: false
|
|
2234
|
-
},
|
|
2235
|
-
doc_paths: [],
|
|
2236
|
-
kind: "terminal"
|
|
2237
|
-
},
|
|
2238
|
-
when: "pubspec.yaml is present at the project root."
|
|
2239
|
-
},
|
|
2240
|
-
{
|
|
2241
|
-
id: "web",
|
|
2242
|
-
label: "Web application",
|
|
2243
|
-
outcome: {
|
|
2244
|
-
kind: "branch",
|
|
2245
|
-
next_question_id: "node-package-manager"
|
|
2246
|
-
},
|
|
2247
|
-
when: "package.json declares no react-native and no expo."
|
|
2248
|
-
}
|
|
2249
|
-
],
|
|
2250
|
-
prompt: "What shape is the host project?",
|
|
2251
|
-
supply_with: "run from the application root, or pass --cwd <path> pointing at it",
|
|
2252
|
-
why_it_is_asked: "the shape of the host project could not be identified, and the install verb, the native linking step and the import target all follow from it"
|
|
2253
|
-
}
|
|
2254
|
-
],
|
|
2255
|
-
schema_version: 1,
|
|
2256
|
-
supply_with: "detected from the project; pass --cwd <path> to point at the application root",
|
|
2257
|
-
title: "Which platform this integration is built on",
|
|
2258
|
-
tree_id: "platform"
|
|
2259
|
-
},
|
|
2260
|
-
{
|
|
2261
|
-
decides: "The set of SDK capability surfaces to mount, and for each one the import target and the optional native peers it binds.",
|
|
2262
|
-
entry_question_id: "which-capability",
|
|
2263
|
-
principles: [
|
|
2264
|
-
{
|
|
2265
|
-
doc_path: "/.well-known/skills/everfur-integration.md#bundle-attach-narrows",
|
|
2266
|
-
id: "entitlement-lattice-subtracts",
|
|
2267
|
-
statement: "The entitlement lattice is born-full and SUBTRACTIVE. The granted set is the top-level grant universe intersected with every ceiling on the path, minus every live deny. Nothing below the top adds anything. Attaching a bundle materialises a ceiling whose allow list is exactly that bundle's contents, so reaching for a bundle to hand a tenant a capability is reaching for the one mechanism that can only restrict. To widen a tenant, widen or reset the ceiling that excludes the key, or lift the deny that matched it.",
|
|
2268
|
-
title: "Attaching a bundle to grant a capability takes capabilities away"
|
|
2269
|
-
},
|
|
2270
|
-
{
|
|
2271
|
-
doc_path: "/.well-known/skills/everfur-integration.md#empty-capability-verdict",
|
|
2272
|
-
id: "empty-verdict-is-a-symptom",
|
|
2273
|
-
statement: "A 200 with an empty capability map is not a denial. The same response covers a tenant with no entitlement scope provisioned, a live deny, a ceiling that excludes the key, a key absent from the grant universe, a runtime gate that is off, and a resolve that could not complete and degraded to the fail-closed empty verdict, which means UNKNOWN rather than denied. They are byte-identical on the wire by design, because a denial reason is not something a device should branch on. Run `everfur diagnose` against the same credential: it returns one machine-readable reason per capability, and that is the thing that separates the causes. A gate cause is an operator action no entitlement change will fix; a not-provisioned cause is a support request; a resolver-unavailable cause means retry; a denied cause is configuration somebody chose.",
|
|
2274
|
-
title: "An empty capability verdict is a symptom, not a diagnosis"
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
doc_path: "/.well-known/skills/everfur-integration.md#wire-message-is-not-copy",
|
|
2278
|
-
id: "wire-message-is-never-rendered",
|
|
2279
|
-
statement: "Every capability surface renders errors, and the tempting field is the wrong one. The upstream text on an error is untrusted, unlocalised, health-adjacent and sanitised by nothing: it can carry provider internals or a URL into somebody's record. The SDK's only protection is structural, the field is non-enumerable so serialisers drop it, and reading it deliberately is what defeats that. Render the display copy the error registry publishes for the code, and never forward the upstream text to a third-party telemetry or crash sink. When a person needs something to quote, quote the request id from the response header.",
|
|
2280
|
-
title: "The wire message is diagnostic text, never something a person sees"
|
|
2281
|
-
},
|
|
2282
|
-
{
|
|
2283
|
-
doc_path: "/reference/capabilities.md",
|
|
2284
|
-
id: "verdict-is-server-authoritative",
|
|
2285
|
-
statement: "The capability list handed back at session mint is advisory and is stated to be advisory; the authoritative verdict comes from the entitlements route and the SDK renders it as received. Do not cache it across users, do not intersect it with a local list of what you think the tenant bought, and do not treat its absence as a denial. Two independent checks have to pass for a surface to work: the entitlement resolves granted, and the runtime gate is on. They fail differently and only one of them is visible in the verdict.",
|
|
2286
|
-
title: "The verdict is resolved server side and never re-folded on the client"
|
|
2287
|
-
}
|
|
2288
|
-
],
|
|
2289
|
-
questions: [
|
|
2290
|
-
{
|
|
2291
|
-
detectable: false,
|
|
2292
|
-
id: "which-capability",
|
|
2293
|
-
options: [
|
|
2294
|
-
{
|
|
2295
|
-
id: "animations",
|
|
2296
|
-
label: "Animated pet state",
|
|
2297
|
-
outcome: {
|
|
2298
|
-
capability_refs: [
|
|
2299
|
-
"animations.state.read"
|
|
2300
|
-
],
|
|
2301
|
-
kind: "blocked",
|
|
2302
|
-
reason: "The animated pet state is surfaced to the Everfur consumer app rather than to the partner widget surface, and its runtime gate is a control-plane switch that is off by default and fails closed. A partner integration has no route to it today.",
|
|
2303
|
-
request_path: "/reference/capabilities.md#capability-animations.state.read",
|
|
2304
|
-
unblock_owner: "support@everfur.com"
|
|
2305
|
-
},
|
|
2306
|
-
when: "The surface shows the animated pet avatar."
|
|
2307
|
-
},
|
|
2308
|
-
{
|
|
2309
|
-
id: "chat",
|
|
2310
|
-
label: "AI vet chat",
|
|
2311
|
-
outcome: {
|
|
2312
|
-
attributes: {
|
|
2313
|
-
dart_library: "package:everfur_sdk/everfur_sdk.dart",
|
|
2314
|
-
native_peers: [],
|
|
2315
|
-
node_subpath: "@everfur/sdk/chat",
|
|
2316
|
-
summary: "Streaming chat surface."
|
|
2317
|
-
},
|
|
2318
|
-
capability_refs: [
|
|
2319
|
-
"chat.conversation.read",
|
|
2320
|
-
"chat.message.create"
|
|
2321
|
-
],
|
|
2322
|
-
doc_paths: [
|
|
2323
|
-
"/reference/capabilities.md#capability-chat.conversation.read",
|
|
2324
|
-
"/reference/capabilities.md#capability-chat.message.create"
|
|
2325
|
-
],
|
|
2326
|
-
kind: "terminal"
|
|
2327
|
-
},
|
|
2328
|
-
when: "The surface shows a conversation with the Everfur assistant."
|
|
2329
|
-
},
|
|
2330
|
-
{
|
|
2331
|
-
id: "entitlements",
|
|
2332
|
-
label: "Entitlement gating for capability surfaces",
|
|
2333
|
-
outcome: {
|
|
2334
|
-
attributes: {
|
|
2335
|
-
dart_library: "package:everfur_sdk/everfur_sdk.dart",
|
|
2336
|
-
native_peers: [],
|
|
2337
|
-
node_subpath: "@everfur/sdk/core",
|
|
2338
|
-
summary: "Entitlement gating for capability surfaces."
|
|
2339
|
-
},
|
|
2340
|
-
capability_refs: [],
|
|
2341
|
-
doc_paths: [
|
|
2342
|
-
"/reference/capabilities.md"
|
|
2343
|
-
],
|
|
2344
|
-
kind: "terminal"
|
|
2345
|
-
},
|
|
2346
|
-
when: "The integration hides or shows its own UI based on what the tenant is entitled to."
|
|
2347
|
-
},
|
|
2348
|
-
{
|
|
2349
|
-
id: "photo",
|
|
2350
|
-
label: "Photo checkup",
|
|
2351
|
-
outcome: {
|
|
2352
|
-
attributes: {
|
|
2353
|
-
dart_library: "package:everfur_sdk/everfur_sdk.dart",
|
|
2354
|
-
native_peers: [
|
|
2355
|
-
"expo-file-system",
|
|
2356
|
-
"expo-image-manipulator",
|
|
2357
|
-
"react-native-vision-camera"
|
|
2358
|
-
],
|
|
2359
|
-
node_subpath: "@everfur/sdk/photo",
|
|
2360
|
-
summary: "Photo capture, resize, and upload."
|
|
2361
|
-
},
|
|
2362
|
-
capability_refs: [
|
|
2363
|
-
"photo.checkup.execute"
|
|
2364
|
-
],
|
|
2365
|
-
doc_paths: [
|
|
2366
|
-
"/reference/capabilities.md#capability-photo.checkup.execute"
|
|
2367
|
-
],
|
|
2368
|
-
kind: "terminal"
|
|
2369
|
-
},
|
|
2370
|
-
when: "The surface captures a skin or eye photo and shows an analysis."
|
|
2371
|
-
},
|
|
2372
|
-
{
|
|
2373
|
-
id: "records",
|
|
2374
|
-
label: "Health record upload and timeline",
|
|
2375
|
-
outcome: {
|
|
2376
|
-
capability_refs: [
|
|
2377
|
-
"records.record.export",
|
|
2378
|
-
"records.record.read",
|
|
2379
|
-
"records.record.share",
|
|
2380
|
-
"records.upload.create"
|
|
2381
|
-
],
|
|
2382
|
-
kind: "blocked",
|
|
2383
|
-
reason: "The partner records surface is mounted behind a control-plane switch that is off by default and read strictly, so every route under it answers 404 rather than 403 until Everfur enables it for your client. A 404 is indistinguishable from a wrong URL, so an integration built against a dark surface reads to a partner as a broken SDK. The two record-movement packs are separately out of reach: export and share are not routed to the partner widget surface at all, and every action in both is never_agent, so no agent path to them exists even for an entitled tenant.",
|
|
2384
|
-
request_path: "/reference/capabilities.md#capability-records.upload.create",
|
|
2385
|
-
unblock_owner: "support@everfur.com"
|
|
2386
|
-
},
|
|
2387
|
-
when: "The surface uploads vet documents or shows a pet's record history."
|
|
2388
|
-
},
|
|
2389
|
-
{
|
|
2390
|
-
id: "video",
|
|
2391
|
-
label: "Gait and mobility video analysis",
|
|
2392
|
-
outcome: {
|
|
2393
|
-
capability_refs: [
|
|
2394
|
-
"video.gait.execute"
|
|
2395
|
-
],
|
|
2396
|
-
kind: "blocked",
|
|
2397
|
-
reason: "Gait analysis is surfaced to the Everfur consumer app and is not routed to the partner widget surface, and its runtime gate is a control-plane switch that is off by default and fails closed. Both have to change before a partner integration can reach it, and neither is a setting a partner can turn on.",
|
|
2398
|
-
request_path: "/reference/capabilities.md#capability-video.gait.execute",
|
|
2399
|
-
unblock_owner: "support@everfur.com"
|
|
2400
|
-
},
|
|
2401
|
-
when: "The surface captures a walking video and shows a mobility result."
|
|
2402
|
-
}
|
|
2403
|
-
],
|
|
2404
|
-
prompt: "Which Everfur capability does this surface mount?",
|
|
2405
|
-
supply_with: "--capabilities chat,entitlements,photo (name every surface you mount)",
|
|
2406
|
-
why_it_is_asked: "which Everfur capabilities this integration uses cannot be read from the project; nothing on disk says which product surface somebody intends to build, and the import target, the optional native peers, and whether the surface is reachable at all differ per capability"
|
|
2407
|
-
}
|
|
2408
|
-
],
|
|
2409
|
-
schema_version: 1,
|
|
2410
|
-
supply_with: "--capabilities chat,entitlements,photo (name every surface you mount)",
|
|
2411
|
-
title: "Which capability surfaces this integration mounts",
|
|
2412
|
-
tree_id: "capabilities"
|
|
2413
|
-
},
|
|
2414
|
-
{
|
|
2415
|
-
decides: "Whether the integration asserts a partner-owned user reference, and therefore whether per-user data is reachable.",
|
|
2416
|
-
entry_question_id: "how-users-are-named",
|
|
2417
|
-
principles: [
|
|
2418
|
-
{
|
|
2419
|
-
doc_path: "/.well-known/skills/everfur-integration.md#user-ref-case",
|
|
2420
|
-
id: "user-ref-is-case-sensitive",
|
|
2421
|
-
statement: "A user reference is an opaque, case-sensitive identifier resolved by exact match, and the session is keyed on the (client, user reference) pair with the case preserved. Lowercasing it anywhere on the path collapses two case-distinct references onto one internal user, which is a cross-user data bleed on read and a cross-user erasure on delete. This is not hypothetical: it happened, and the fix was to remove the case fold. Send the reference exactly as your system stores it, including in your own cache keys. If your identifiers are genuinely case-insensitive upstream, normalise them ONCE where they are minted, before they ever become a user reference.",
|
|
2422
|
-
title: "user_ref is case sensitive, and folding it merges two people"
|
|
2423
|
-
},
|
|
2424
|
-
{
|
|
2425
|
-
doc_path: "/reference/capabilities.md",
|
|
2426
|
-
id: "ownership-fence-is-the-composite",
|
|
2427
|
-
statement: "Every per-user read is fenced on the composite of the resolved user and the pet, not on the tenant alone. With no user reference asserted nothing owned by a specific end user is reachable, by construction rather than by policy. That is what makes the anonymous mode safe and what makes a guessable user reference dangerous: the reference IS the tenancy claim for everything below it, so it must be stable per person and must not be enumerable.",
|
|
2428
|
-
title: "The ownership fence is the (user, pet) composite"
|
|
2429
|
-
}
|
|
2430
|
-
],
|
|
2431
|
-
questions: [
|
|
2432
|
-
{
|
|
2433
|
-
detectable: false,
|
|
2434
|
-
id: "how-users-are-named",
|
|
2435
|
-
options: [
|
|
2436
|
-
{
|
|
2437
|
-
id: "anonymous",
|
|
2438
|
-
label: "No end user is asserted",
|
|
2439
|
-
outcome: {
|
|
2440
|
-
attributes: {
|
|
2441
|
-
reaches_per_user_data: false,
|
|
2442
|
-
session_mint: "Not applicable. A mint requires a non-empty user reference, so there is nothing to mint for a surface that asserts none.",
|
|
2443
|
-
user_ref_header: null
|
|
2444
|
-
},
|
|
2445
|
-
doc_paths: [
|
|
2446
|
-
"/reference/capabilities.md"
|
|
2447
|
-
],
|
|
2448
|
-
kind: "terminal"
|
|
2449
|
-
},
|
|
2450
|
-
when: "The surface shows nothing owned by a specific person: a marketing page, a demo, or a logged-out state."
|
|
2451
|
-
},
|
|
2452
|
-
{
|
|
2453
|
-
id: "partner-user-ref",
|
|
2454
|
-
label: "The partner asserts its own stable user reference",
|
|
2455
|
-
outcome: {
|
|
2456
|
-
attributes: {
|
|
2457
|
-
reaches_per_user_data: true,
|
|
2458
|
-
session_mint: "A minted session is how the tenancy claim is made from your backend instead of from the device, and it is mandatory for a client configured to require partner sessions. Answer the session-mint-language tree next.",
|
|
2459
|
-
user_ref_case_sensitive: true,
|
|
2460
|
-
user_ref_forbidden_characters: "@",
|
|
2461
|
-
user_ref_header: "x-everfur-user-ref",
|
|
2462
|
-
user_ref_max_length: 128,
|
|
2463
|
-
user_role_header: "x-everfur-user-role"
|
|
2464
|
-
},
|
|
2465
|
-
doc_paths: [
|
|
2466
|
-
"/.well-known/skills/everfur-integration.md#user-ref-case"
|
|
2467
|
-
],
|
|
2468
|
-
kind: "terminal",
|
|
2469
|
-
then_ask_tree_id: "session-mint-language"
|
|
2470
|
-
},
|
|
2471
|
-
when: "The integration already has a signed-in person and a stable identifier for them, and the surface shows that person's pets, conversations or records."
|
|
2472
|
-
}
|
|
2473
|
-
],
|
|
2474
|
-
prompt: "How does this integration identify the person using it?",
|
|
2475
|
-
supply_with: "--identity partner-user-ref | anonymous",
|
|
2476
|
-
why_it_is_asked: "how this integration identifies end users cannot be read from the project, and the two answers reach different data; guessing wrong in one direction builds a surface that shows nothing, and in the other builds one that asserts an identity the partner never intended to assert"
|
|
2477
|
-
}
|
|
2478
|
-
],
|
|
2479
|
-
schema_version: 1,
|
|
2480
|
-
supply_with: "--identity partner-user-ref | anonymous",
|
|
2481
|
-
title: "How end users are named on the wire",
|
|
2482
|
-
tree_id: "identity-mode"
|
|
2483
|
-
},
|
|
2484
|
-
{
|
|
2485
|
-
decides: "Whether the integration mints sessions through the first-party server package or by calling the mint route directly.",
|
|
2486
|
-
entry_question_id: "backend-language",
|
|
2487
|
-
principles: [
|
|
2488
|
-
{
|
|
2489
|
-
doc_path: "/.well-known/skills/everfur-integration.md#server-only-minter",
|
|
2490
|
-
id: "minter-is-server-only",
|
|
2491
|
-
statement: "The mint is the one request on the whole surface that legitimately carries both credentials at once, and a device must never hold the secret half. The `/server` subpath is what enforces that structurally: it declares no react-native export condition, so a mobile bundler cannot resolve it. Import the same function from the package root or a client subpath and the protection is gone, because the fence was the subpath, not the function. If a bundler ever resolves the server subpath into a device build, treat it as a leaked secret and rotate rather than shipping the build.",
|
|
2492
|
-
title: "The session minter is imported only from the server subpath"
|
|
2493
|
-
},
|
|
2494
|
-
{
|
|
2495
|
-
doc_path: "/.well-known/skills/everfur-integration.md#user-ref-case",
|
|
2496
|
-
id: "mint-carries-the-identity",
|
|
2497
|
-
statement: "The reference travels in the mint body, and the same validation rule applies there as on the per-request header: stripped, non-empty, at most 128 characters, printable, no `@`. The one difference is the empty case, which is a 400 at the mint because a session cannot be minted for nobody, while an empty header on the per-request path falls back to an anonymous identity. Do not build a mint call that sends an empty reference and expects anonymity: it is a rejection, not a fallback.",
|
|
2498
|
-
title: "The mint body carries the user reference, and the rules are the same on both paths"
|
|
2499
|
-
},
|
|
2500
|
-
{
|
|
2501
|
-
doc_path: "/reference/errors.md",
|
|
2502
|
-
id: "mint-failures-are-one-opaque-401",
|
|
2503
|
-
statement: "Wrong secret, wrong publishable key, a mismatched pair from two different tenants, an inactive client, a lookup timeout: all of them return the same 401 with no detail, on purpose, so the route is not an oracle. Do not build retry logic that branches on why the mint failed, because the wire does not tell you. Correlate with the request id and read the diagnostics route instead.",
|
|
2504
|
-
title: "Every credential failure at the mint is one indistinguishable 401"
|
|
2505
|
-
}
|
|
2506
|
-
],
|
|
2507
|
-
questions: [
|
|
2508
|
-
{
|
|
2509
|
-
detectable: false,
|
|
2510
|
-
id: "backend-language",
|
|
2511
|
-
options: [
|
|
2512
|
-
{
|
|
2513
|
-
id: "go",
|
|
2514
|
-
label: "Go",
|
|
2515
|
-
outcome: {
|
|
2516
|
-
kind: "branch",
|
|
2517
|
-
next_question_id: "direct-mint-call"
|
|
2518
|
-
},
|
|
2519
|
-
when: "The backend is a Go service."
|
|
2520
|
-
},
|
|
2521
|
-
{
|
|
2522
|
-
id: "node",
|
|
2523
|
-
label: "Node or TypeScript",
|
|
2524
|
-
outcome: {
|
|
2525
|
-
attributes: {
|
|
2526
|
-
import_from_root_is_a_defect: true,
|
|
2527
|
-
import_subpath: "@everfur/sdk/server",
|
|
2528
|
-
package: "@everfur/sdk",
|
|
2529
|
-
secret_source: "an environment variable or secret manager on the server, never a bundled constant"
|
|
2530
|
-
},
|
|
2531
|
-
doc_paths: [
|
|
2532
|
-
"/.well-known/skills/everfur-integration.md#server-only-minter"
|
|
2533
|
-
],
|
|
2534
|
-
kind: "terminal"
|
|
2535
|
-
},
|
|
2536
|
-
when: "The backend runs on Node and can install from the npm registry."
|
|
2537
|
-
},
|
|
2538
|
-
{
|
|
2539
|
-
id: "other-language",
|
|
2540
|
-
label: "Anything else",
|
|
2541
|
-
outcome: {
|
|
2542
|
-
kind: "branch",
|
|
2543
|
-
next_question_id: "direct-mint-call"
|
|
2544
|
-
},
|
|
2545
|
-
when: "The backend is any other language, or the mint lives in an edge function."
|
|
2546
|
-
},
|
|
2547
|
-
{
|
|
2548
|
-
id: "python",
|
|
2549
|
-
label: "Python",
|
|
2550
|
-
outcome: {
|
|
2551
|
-
kind: "branch",
|
|
2552
|
-
next_question_id: "direct-mint-call"
|
|
2553
|
-
},
|
|
2554
|
-
when: "The backend is a Python service."
|
|
2555
|
-
}
|
|
2556
|
-
],
|
|
2557
|
-
prompt: "What is the backend that will hold the secret and mint sessions written in?",
|
|
2558
|
-
supply_with: "--backend node | python | go | other-language",
|
|
2559
|
-
why_it_is_asked: "this integration asserts its own user reference, so a session has to be minted server side, and only one language has a first-party minter; the rest is the same HTTP call, and saying so is more useful than pretending a package exists"
|
|
2560
|
-
},
|
|
2561
|
-
{
|
|
2562
|
-
detectable: false,
|
|
2563
|
-
id: "direct-mint-call",
|
|
2564
|
-
options: [
|
|
2565
|
-
{
|
|
2566
|
-
id: "call-the-mint-route",
|
|
2567
|
-
label: "Call the mint route from the backend",
|
|
2568
|
-
outcome: {
|
|
2569
|
-
attributes: {
|
|
2570
|
-
body_fields: [
|
|
2571
|
-
"ttl_seconds",
|
|
2572
|
-
"user_ref"
|
|
2573
|
-
],
|
|
2574
|
-
failure_mode: "one opaque 401 for every credential failure",
|
|
2575
|
-
publishable_header: "x-everfur-partner-key",
|
|
2576
|
-
route: "POST /api/v1/widget/v1/sessions",
|
|
2577
|
-
secret_header: "x-everfur-partner-secret-key",
|
|
2578
|
-
secret_source: "an environment variable or secret manager on the server, never a bundled constant",
|
|
2579
|
-
ttl_behaviour: "ttl_seconds is clamped server side to the configured range; an absent value takes the default"
|
|
2580
|
-
},
|
|
2581
|
-
doc_paths: [
|
|
2582
|
-
"/.well-known/skills/everfur-integration.md#server-only-minter",
|
|
2583
|
-
"/reference/errors.md"
|
|
2584
|
-
],
|
|
2585
|
-
kind: "terminal"
|
|
2586
|
-
},
|
|
2587
|
-
when: "Always, for a language with no first-party minter."
|
|
2588
|
-
}
|
|
2589
|
-
],
|
|
2590
|
-
prompt: "There is no first-party minter for this language. Confirm the direct call.",
|
|
2591
|
-
supply_with: null,
|
|
2592
|
-
why_it_is_asked: "three languages converge here because the answer is identical for all of them, and stating it once is what keeps the three from drifting apart"
|
|
2593
|
-
}
|
|
2594
|
-
],
|
|
2595
|
-
schema_version: 1,
|
|
2596
|
-
supply_with: "--backend node | python | go | other-language",
|
|
2597
|
-
title: "Which language mints sessions on the partner backend",
|
|
2598
|
-
tree_id: "session-mint-language"
|
|
2599
|
-
}
|
|
2600
|
-
]
|
|
2601
|
-
},
|
|
2602
|
-
"errors.yaml": {
|
|
2603
|
-
errors: [
|
|
2604
|
-
{
|
|
2605
|
-
code: "AccessDeniedException",
|
|
2606
|
-
confirmed_on_wire: true,
|
|
2607
|
-
display: "You do not have access to that.",
|
|
2608
|
-
doc_url: "/errors#AccessDeniedException",
|
|
2609
|
-
envelope: "http_envelope",
|
|
2610
|
-
http_status: 403,
|
|
2611
|
-
naming_family: "pascal_exception",
|
|
2612
|
-
note: "Emitted by the report-page tier gate with extra fields current_tier, required_tier, page_id and upgrade_url, and as the status-map default for any other 403 raised with a string detail.",
|
|
2613
|
-
retryable: false,
|
|
2614
|
-
surfaces: [
|
|
2615
|
-
"consumer_app"
|
|
2616
|
-
]
|
|
2617
|
-
},
|
|
2618
|
-
{
|
|
2619
|
-
code: "AuthorizationException",
|
|
2620
|
-
confirmed_on_wire: true,
|
|
2621
|
-
display: "Your session is not valid. Sign in again to continue.",
|
|
2622
|
-
doc_url: "/errors#AuthorizationException",
|
|
2623
|
-
envelope: "http_envelope",
|
|
2624
|
-
http_status: 401,
|
|
2625
|
-
naming_family: "pascal_exception",
|
|
2626
|
-
note: "The generic 401. The widget partner-key path emits this with a string detail; the bearer path emits authExpired instead.",
|
|
2627
|
-
retryable: false,
|
|
2628
|
-
surfaces: [
|
|
2629
|
-
"consumer_app",
|
|
2630
|
-
"partner_widget"
|
|
2631
|
-
]
|
|
2632
|
-
},
|
|
2633
|
-
{
|
|
2634
|
-
code: "ConflictException",
|
|
2635
|
-
confirmed_on_wire: true,
|
|
2636
|
-
display: "That is already running. Wait for it to finish, then try again.",
|
|
2637
|
-
doc_url: "/errors#ConflictException",
|
|
2638
|
-
envelope: "http_envelope",
|
|
2639
|
-
http_status: 409,
|
|
2640
|
-
naming_family: "pascal_exception",
|
|
2641
|
-
note: "On the widget streaming routes this is the single-flight fence: register_stream does an atomic check-and-set and rejects a second concurrent stream for the same conversation. Retryable once the first stream ends, not immediately.",
|
|
2642
|
-
retryable: true,
|
|
2643
|
-
surfaces: [
|
|
2644
|
-
"partner_widget"
|
|
2645
|
-
]
|
|
2646
|
-
},
|
|
2647
|
-
{
|
|
2648
|
-
code: "DAILY_QUOTA_EXCEEDED",
|
|
2649
|
-
confirmed_on_wire: true,
|
|
2650
|
-
display: "You have used today's allowance for this. It resets tomorrow.",
|
|
2651
|
-
doc_url: "/errors#DAILY_QUOTA_EXCEEDED",
|
|
2652
|
-
envelope: "http_envelope",
|
|
2653
|
-
http_status: 429,
|
|
2654
|
-
naming_family: "screaming_snake",
|
|
2655
|
-
note: "The per-endpoint guardrail, a different counter from the tier-wide daily quota. Its envelope is the leaner {detail, code, endpoint, daily_limit}; note it uses `detail`, not `message`, and carries no request_id.",
|
|
2656
|
-
retryable: true,
|
|
2657
|
-
surfaces: [
|
|
2658
|
-
"consumer_app"
|
|
2659
|
-
]
|
|
2660
|
-
},
|
|
2661
|
-
{
|
|
2662
|
-
code: "IDEMPOTENCY_KEY_IN_FLIGHT",
|
|
2663
|
-
confirmed_on_wire: true,
|
|
2664
|
-
display: "That is still processing. Give it a moment.",
|
|
2665
|
-
doc_url: "/errors#IDEMPOTENCY_KEY_IN_FLIGHT",
|
|
2666
|
-
envelope: "http_envelope",
|
|
2667
|
-
http_status: 409,
|
|
2668
|
-
naming_family: "screaming_snake",
|
|
2669
|
-
note: "The same Idempotency-Key is already claimed and running. Retry the SAME key after a short backoff to pick up the recorded response; do not mint a new key, which would duplicate the write. No widget router opts into the idempotent route class today, so this is not reachable on the partner surface.",
|
|
2670
|
-
retryable: true,
|
|
2671
|
-
surfaces: [
|
|
2672
|
-
"consumer_app"
|
|
2673
|
-
]
|
|
2674
|
-
},
|
|
2675
|
-
{
|
|
2676
|
-
code: "IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST",
|
|
2677
|
-
confirmed_on_wire: true,
|
|
2678
|
-
display: "That request could not be repeated safely. Start it again.",
|
|
2679
|
-
doc_url: "/errors#IDEMPOTENCY_KEY_REUSED_DIFFERENT_REQUEST",
|
|
2680
|
-
envelope: "http_envelope",
|
|
2681
|
-
http_status: 409,
|
|
2682
|
-
naming_family: "screaming_snake",
|
|
2683
|
-
note: "The key was reused with a different request body. This is a client bug, not a transient fault. Mint a fresh key for the new body.",
|
|
2684
|
-
retryable: false,
|
|
2685
|
-
surfaces: [
|
|
2686
|
-
"consumer_app"
|
|
2687
|
-
]
|
|
2688
|
-
},
|
|
2689
|
-
{
|
|
2690
|
-
code: "InternalFailureException",
|
|
2691
|
-
confirmed_on_wire: true,
|
|
2692
|
-
display: "Something went wrong on our side. Try again in a moment.",
|
|
2693
|
-
doc_url: "/errors#InternalFailureException",
|
|
2694
|
-
envelope: "http_envelope",
|
|
2695
|
-
http_status: 500,
|
|
2696
|
-
naming_family: "pascal_exception",
|
|
2697
|
-
note: "Also the fallback for any status with no entry in the map, so a 502 arrives under this code. The message is masked to a fixed string for every 5xx except 501, so it never carries diagnostic detail. Quote request_id when reporting.",
|
|
2698
|
-
retryable: true,
|
|
2699
|
-
surfaces: [
|
|
2700
|
-
"consumer_app",
|
|
2701
|
-
"partner_widget"
|
|
2702
|
-
]
|
|
2703
|
-
},
|
|
2704
|
-
{
|
|
2705
|
-
code: "NotImplementedException",
|
|
2706
|
-
confirmed_on_wire: true,
|
|
2707
|
-
display: "That is not available yet.",
|
|
2708
|
-
doc_url: "/errors#NotImplementedException",
|
|
2709
|
-
envelope: "http_envelope",
|
|
2710
|
-
http_status: 501,
|
|
2711
|
-
naming_family: "pascal_exception",
|
|
2712
|
-
note: "The one 5xx whose message is passed through unmasked, because it describes product state rather than a fault.",
|
|
2713
|
-
retryable: false,
|
|
2714
|
-
surfaces: [
|
|
2715
|
-
"consumer_app"
|
|
2716
|
-
]
|
|
2717
|
-
},
|
|
2718
|
-
{
|
|
2719
|
-
code: "PaymentRequiredException",
|
|
2720
|
-
confirmed_on_wire: false,
|
|
2721
|
-
display: "This needs a plan upgrade.",
|
|
2722
|
-
doc_url: "/errors#PaymentRequiredException",
|
|
2723
|
-
envelope: "http_envelope",
|
|
2724
|
-
http_status: 402,
|
|
2725
|
-
naming_family: "pascal_exception",
|
|
2726
|
-
note: "Present in the status map, but every 402 traced in this pass is emitted by the typed entitlement handler as TIER_UPGRADE_REQUIRED, not as this generic code.",
|
|
2727
|
-
retryable: false,
|
|
2728
|
-
surfaces: [
|
|
2729
|
-
"consumer_app"
|
|
2730
|
-
]
|
|
2731
|
-
},
|
|
2732
|
-
{
|
|
2733
|
-
code: "ResourceNotFoundException",
|
|
2734
|
-
confirmed_on_wire: true,
|
|
2735
|
-
display: "We could not find that.",
|
|
2736
|
-
doc_url: "/errors#ResourceNotFoundException",
|
|
2737
|
-
envelope: "http_envelope",
|
|
2738
|
-
http_status: 404,
|
|
2739
|
-
naming_family: "pascal_exception",
|
|
2740
|
-
note: "Load-bearing ambiguity, by design. A dark runtime gate answers 404 so a disabled surface is indistinguishable from a missing resource, and an ownership miss answers 404 so another tenant's resource is invisible rather than merely forbidden. A client must not infer existence from a 404.",
|
|
2741
|
-
retryable: false,
|
|
2742
|
-
surfaces: [
|
|
2743
|
-
"consumer_app",
|
|
2744
|
-
"partner_widget"
|
|
2745
|
-
]
|
|
2746
|
-
},
|
|
2747
|
-
{
|
|
2748
|
-
code: "ServiceUnavailableException",
|
|
2749
|
-
confirmed_on_wire: true,
|
|
2750
|
-
display: "That service is briefly unavailable. Try again shortly.",
|
|
2751
|
-
doc_url: "/errors#ServiceUnavailableException",
|
|
2752
|
-
envelope: "http_envelope",
|
|
2753
|
-
http_status: 503,
|
|
2754
|
-
naming_family: "pascal_exception",
|
|
2755
|
-
note: "The status-map default for 503. The widget dependencies raise the dict-detail code `unavailable` instead, so a widget client sees that one.",
|
|
2756
|
-
retryable: true,
|
|
2757
|
-
surfaces: [
|
|
2758
|
-
"consumer_app"
|
|
2759
|
-
]
|
|
2760
|
-
},
|
|
2761
|
-
{
|
|
2762
|
-
code: "TIER_UPGRADE_REQUIRED",
|
|
2763
|
-
confirmed_on_wire: true,
|
|
2764
|
-
display: "This is part of a paid plan.",
|
|
2765
|
-
doc_url: "/errors#TIER_UPGRADE_REQUIRED",
|
|
2766
|
-
envelope: "http_envelope",
|
|
2767
|
-
http_status: 402,
|
|
2768
|
-
naming_family: "screaming_snake",
|
|
2769
|
-
note: "The store paywall gate. Extends the envelope with capability, current_tier, limit, used, paywall_cta and upgrade_url. Distinct from the 403 AccessDeniedException tier gate on report pages.",
|
|
2770
|
-
retryable: false,
|
|
2771
|
-
surfaces: [
|
|
2772
|
-
"consumer_app"
|
|
2773
|
-
]
|
|
2774
|
-
},
|
|
2775
|
-
{
|
|
2776
|
-
code: "ThrottlingException",
|
|
2777
|
-
confirmed_on_wire: true,
|
|
2778
|
-
display: "You have reached the limit for now. Try again later.",
|
|
2779
|
-
doc_url: "/errors#ThrottlingException",
|
|
2780
|
-
envelope: "http_envelope",
|
|
2781
|
-
http_status: 429,
|
|
2782
|
-
naming_family: "pascal_exception",
|
|
2783
|
-
note: "The quota handler extends the envelope with daily_limit, used and resets_at, and adds tier when the exception carries one. resets_at is the only reliable retry signal; there is no Retry-After on this path.",
|
|
2784
|
-
retryable: true,
|
|
2785
|
-
surfaces: [
|
|
2786
|
-
"consumer_app",
|
|
2787
|
-
"partner_widget"
|
|
2788
|
-
]
|
|
2789
|
-
},
|
|
2790
|
-
{
|
|
2791
|
-
code: "ValidationException",
|
|
2792
|
-
confirmed_on_wire: true,
|
|
2793
|
-
display: "Something in that request was not right. Check the details and try again.",
|
|
2794
|
-
doc_url: "/errors#ValidationException",
|
|
2795
|
-
envelope: "http_envelope",
|
|
2796
|
-
http_status: 400,
|
|
2797
|
-
naming_family: "pascal_exception",
|
|
2798
|
-
note: "The default code for 400, 413, 415 and 422 alike, so payload-too-large and unsupported-media-type are indistinguishable from a field error by code. Branch on http_status as well as code.",
|
|
2799
|
-
retryable: false,
|
|
2800
|
-
surfaces: [
|
|
2801
|
-
"consumer_app",
|
|
2802
|
-
"partner_widget"
|
|
2803
|
-
]
|
|
2804
|
-
},
|
|
2805
|
-
{
|
|
2806
|
-
code: "accessDenied",
|
|
2807
|
-
confirmed_on_wire: true,
|
|
2808
|
-
display: "That request could not be authorised.",
|
|
2809
|
-
doc_url: "/errors#accessDenied",
|
|
2810
|
-
envelope: "http_envelope",
|
|
2811
|
-
http_status: 403,
|
|
2812
|
-
naming_family: "camel",
|
|
2813
|
-
note: "The caller presented BOTH a session bearer and a publishable partner key. Exactly one is correct per request. Retrying with both set will fail identically.",
|
|
2814
|
-
retryable: false,
|
|
2815
|
-
surfaces: [
|
|
2816
|
-
"partner_widget"
|
|
2817
|
-
]
|
|
2818
|
-
},
|
|
2819
|
-
{
|
|
2820
|
-
code: "authExpired",
|
|
2821
|
-
confirmed_on_wire: true,
|
|
2822
|
-
display: "Your session expired. Reconnecting.",
|
|
2823
|
-
doc_url: "/errors#authExpired",
|
|
2824
|
-
envelope: "http_envelope",
|
|
2825
|
-
http_status: 401,
|
|
2826
|
-
naming_family: "camel",
|
|
2827
|
-
note: "The one 401 a client should respond to by minting a fresh session token and replaying the request exactly once. Emitted for a bad signature, an expired token, a wrong typ or issuer, a token missing a required claim, and for a bearer whose signing key has since been revoked. Do not loop: a second authExpired on the replay is terminal.",
|
|
2828
|
-
retryable: true,
|
|
2829
|
-
surfaces: [
|
|
2830
|
-
"partner_widget"
|
|
2831
|
-
]
|
|
2832
|
-
},
|
|
2833
|
-
{
|
|
2834
|
-
code: "internal_error",
|
|
2835
|
-
confirmed_on_wire: true,
|
|
2836
|
-
display: "Something went wrong on our side.",
|
|
2837
|
-
doc_url: "/errors#sse-internal_error",
|
|
2838
|
-
envelope: "sse_frame",
|
|
2839
|
-
http_status: 200,
|
|
2840
|
-
naming_family: "snake",
|
|
2841
|
-
note: "Emitted with retryable false. The frame carries no request_id, so there is nothing for a client to quote in a support report; correlate by timestamp and conversation id instead. Everfur is tracking the gap.",
|
|
2842
|
-
retryable: false,
|
|
2843
|
-
surfaces: [
|
|
2844
|
-
"partner_widget"
|
|
2845
|
-
]
|
|
2846
|
-
},
|
|
2847
|
-
{
|
|
2848
|
-
code: "invalidRequest",
|
|
2849
|
-
confirmed_on_wire: true,
|
|
2850
|
-
display: "This app needs to be updated to keep using Everfur.",
|
|
2851
|
-
doc_url: "/errors#invalidRequest",
|
|
2852
|
-
envelope: "http_envelope",
|
|
2853
|
-
http_status: 400,
|
|
2854
|
-
naming_family: "camel",
|
|
2855
|
-
note: "The SDK min-version and wire-contract gate. Deliberately 400 rather than 401, 403 or 426, so it neither trips refresh machinery nor lands as an unmapped status. The response carries a `reason` alongside the message so the client can say what specifically is out of date. Inert while the gate flag is off.",
|
|
2856
|
-
retryable: false,
|
|
2857
|
-
surfaces: [
|
|
2858
|
-
"partner_widget"
|
|
2859
|
-
]
|
|
2860
|
-
},
|
|
2861
|
-
{
|
|
2862
|
-
code: "not_found",
|
|
2863
|
-
confirmed_on_wire: true,
|
|
2864
|
-
display: "We could not find that conversation.",
|
|
2865
|
-
doc_url: "/errors#sse-not_found",
|
|
2866
|
-
envelope: "sse_frame",
|
|
2867
|
-
http_status: 200,
|
|
2868
|
-
naming_family: "snake",
|
|
2869
|
-
note: "The stream had already returned 200 and its headers when the failure was discovered, so the HTTP status cannot express it. A client parsing only status codes will read this as success. Parse every data frame for an `error` key.",
|
|
2870
|
-
retryable: false,
|
|
2871
|
-
surfaces: [
|
|
2872
|
-
"partner_widget"
|
|
2873
|
-
]
|
|
2874
|
-
},
|
|
2875
|
-
{
|
|
2876
|
-
code: "session_required",
|
|
2877
|
-
confirmed_on_wire: true,
|
|
2878
|
-
display: "This app needs to be updated to keep using Everfur.",
|
|
2879
|
-
doc_url: "/errors#session_required",
|
|
2880
|
-
envelope: "http_envelope",
|
|
2881
|
-
http_status: 403,
|
|
2882
|
-
naming_family: "snake",
|
|
2883
|
-
note: "An unsigned caller claimed an identity on a tenant that now requires signed sessions. Not retryable as sent: the integration must mint a session token through POST /widget/v1/sessions and carry the bearer instead.",
|
|
2884
|
-
retryable: false,
|
|
2885
|
-
surfaces: [
|
|
2886
|
-
"partner_widget"
|
|
2887
|
-
]
|
|
2888
|
-
},
|
|
2889
|
-
{
|
|
2890
|
-
code: "timeout",
|
|
2891
|
-
confirmed_on_wire: true,
|
|
2892
|
-
display: "That took too long. Try again.",
|
|
2893
|
-
doc_url: "/errors#sse-timeout",
|
|
2894
|
-
envelope: "sse_frame",
|
|
2895
|
-
http_status: 200,
|
|
2896
|
-
naming_family: "snake",
|
|
2897
|
-
note: "Emitted with retryable true when the stream times out or is cancelled. A partial assistant turn may already have been persisted, so a retry can produce a visible duplicate; reconcile against the transcript rather than assuming nothing was written.",
|
|
2898
|
-
retryable: true,
|
|
2899
|
-
surfaces: [
|
|
2900
|
-
"partner_widget"
|
|
2901
|
-
]
|
|
2902
|
-
},
|
|
2903
|
-
{
|
|
2904
|
-
code: "unavailable",
|
|
2905
|
-
confirmed_on_wire: true,
|
|
2906
|
-
display: "We are having trouble reaching that right now. Try again shortly.",
|
|
2907
|
-
doc_url: "/errors#unavailable",
|
|
2908
|
-
envelope: "http_envelope",
|
|
2909
|
-
http_status: 503,
|
|
2910
|
-
naming_family: "snake",
|
|
2911
|
-
note: "Deliberately a 503 and not a 401. It means the server could not decide, because the signing secret was unresolvable or the key-revocation check timed out. Answering 401 here would spin the client's refresh loop against a server fault, so a client must NOT refresh on this code; back off and retry the same request.",
|
|
2912
|
-
retryable: true,
|
|
2913
|
-
surfaces: [
|
|
2914
|
-
"partner_widget"
|
|
2915
|
-
]
|
|
2916
|
-
}
|
|
2917
|
-
],
|
|
2918
|
-
schema_version: 1
|
|
2919
|
-
}
|
|
2920
|
-
}};
|
|
2921
|
-
|
|
2922
|
-
// src/contract/bundled.ts
|
|
2923
|
-
var BUNDLED_CONTRACT = contract_public_default;
|
|
2924
|
-
function bundledContractGeneratedAt() {
|
|
2925
|
-
const doc = contract_public_default;
|
|
2926
|
-
return typeof doc.generatedAt === "string" ? doc.generatedAt : "unknown";
|
|
2927
|
-
}
|
|
2928
|
-
function bundledContractDigest() {
|
|
2929
|
-
const doc = contract_public_default;
|
|
2930
|
-
const value = doc.digest?.actual ?? doc.digest?.expected;
|
|
2931
|
-
return typeof value === "string" ? value : "unknown";
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
// src/commands/plan/trees.ts
|
|
2935
|
-
var DECISION_TREES_DOCUMENT_KEY = "decision-trees.yaml";
|
|
2936
|
-
var PLATFORM_TREE = "platform";
|
|
2937
|
-
var CAPABILITIES_TREE = "capabilities";
|
|
2938
|
-
var IDENTITY_TREE = "identity-mode";
|
|
2939
|
-
var SESSION_MINT_TREE = "session-mint-language";
|
|
2940
|
-
var PROJECT_SHAPE_QUESTION = "project-shape";
|
|
2941
|
-
var PACKAGE_MANAGER_QUESTION = "node-package-manager";
|
|
2942
|
-
var CAPABILITY_QUESTION = "which-capability";
|
|
2943
|
-
var IDENTITY_QUESTION = "how-users-are-named";
|
|
2944
|
-
var BACKEND_LANGUAGE_QUESTION = "backend-language";
|
|
2945
|
-
function readString2(record, key) {
|
|
2946
|
-
const value = record[key];
|
|
2947
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2948
|
-
}
|
|
2949
|
-
function readStringList2(record, key) {
|
|
2950
|
-
const value = record[key];
|
|
2951
|
-
if (!Array.isArray(value)) return Object.freeze([]);
|
|
2952
|
-
return Object.freeze(value.filter((item) => typeof item === "string"));
|
|
2953
|
-
}
|
|
2954
|
-
function readAttributes(value) {
|
|
2955
|
-
if (!isJsonObject(value)) return Object.freeze({});
|
|
2956
|
-
const attributes = {};
|
|
2957
|
-
for (const [key, item] of Object.entries(value)) {
|
|
2958
|
-
if (item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
2959
|
-
attributes[key] = item;
|
|
2960
|
-
continue;
|
|
2961
|
-
}
|
|
2962
|
-
if (Array.isArray(item) && item.every((entry) => typeof entry === "string")) {
|
|
2963
|
-
attributes[key] = Object.freeze([...item]);
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
return Object.freeze(attributes);
|
|
2967
|
-
}
|
|
2968
|
-
function outcomeFromRow(row) {
|
|
2969
|
-
if (!isJsonObject(row)) return null;
|
|
2970
|
-
const kind = readString2(row, "kind");
|
|
2971
|
-
if (kind === "branch") {
|
|
2972
|
-
const nextQuestionId = readString2(row, "next_question_id");
|
|
2973
|
-
return nextQuestionId === null ? null : { kind, nextQuestionId };
|
|
2974
|
-
}
|
|
2975
|
-
if (kind === "terminal") {
|
|
2976
|
-
return Object.freeze({
|
|
2977
|
-
kind,
|
|
2978
|
-
attributes: readAttributes(row["attributes"]),
|
|
2979
|
-
docPaths: readStringList2(row, "doc_paths"),
|
|
2980
|
-
capabilityRefs: readStringList2(row, "capability_refs"),
|
|
2981
|
-
thenAskTreeId: readString2(row, "then_ask_tree_id")
|
|
2982
|
-
});
|
|
2983
|
-
}
|
|
2984
|
-
if (kind === "blocked") {
|
|
2985
|
-
const reason = readString2(row, "reason");
|
|
2986
|
-
const unblockOwner = readString2(row, "unblock_owner");
|
|
2987
|
-
const requestPath = readString2(row, "request_path");
|
|
2988
|
-
if (reason === null || unblockOwner === null || requestPath === null) return null;
|
|
2989
|
-
return Object.freeze({
|
|
2990
|
-
kind,
|
|
2991
|
-
reason,
|
|
2992
|
-
unblockOwner,
|
|
2993
|
-
requestPath,
|
|
2994
|
-
capabilityRefs: readStringList2(row, "capability_refs")
|
|
2995
|
-
});
|
|
2996
|
-
}
|
|
2997
|
-
return null;
|
|
2998
|
-
}
|
|
2999
|
-
function optionFromRow(row) {
|
|
3000
|
-
if (!isJsonObject(row)) return null;
|
|
3001
|
-
const id = readString2(row, "id");
|
|
3002
|
-
if (id === null) return null;
|
|
3003
|
-
const outcome = outcomeFromRow(row["outcome"]);
|
|
3004
|
-
if (outcome === null) return null;
|
|
3005
|
-
return Object.freeze({
|
|
3006
|
-
id,
|
|
3007
|
-
label: readString2(row, "label") ?? id,
|
|
3008
|
-
when: readString2(row, "when") ?? "",
|
|
3009
|
-
outcome
|
|
3010
|
-
});
|
|
3011
|
-
}
|
|
3012
|
-
function questionFromRow(row) {
|
|
3013
|
-
if (!isJsonObject(row)) return null;
|
|
3014
|
-
const id = readString2(row, "id");
|
|
3015
|
-
if (id === null) return null;
|
|
3016
|
-
const rawOptions = Array.isArray(row["options"]) ? row["options"] : [];
|
|
3017
|
-
const options = rawOptions.map(optionFromRow).filter((option) => option !== null).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
3018
|
-
if (options.length === 0) return null;
|
|
3019
|
-
return Object.freeze({
|
|
3020
|
-
id,
|
|
3021
|
-
prompt: readString2(row, "prompt") ?? "",
|
|
3022
|
-
whyItIsAsked: readString2(row, "why_it_is_asked") ?? "",
|
|
3023
|
-
supplyWith: readString2(row, "supply_with"),
|
|
3024
|
-
detectable: row["detectable"] === true,
|
|
3025
|
-
options: Object.freeze(options)
|
|
3026
|
-
});
|
|
3027
|
-
}
|
|
3028
|
-
function principleFromRow(row) {
|
|
3029
|
-
if (!isJsonObject(row)) return null;
|
|
3030
|
-
const id = readString2(row, "id");
|
|
3031
|
-
const statement = readString2(row, "statement");
|
|
3032
|
-
if (id === null || statement === null) return null;
|
|
3033
|
-
return Object.freeze({
|
|
3034
|
-
id,
|
|
3035
|
-
title: readString2(row, "title") ?? id,
|
|
3036
|
-
statement,
|
|
3037
|
-
docPath: readString2(row, "doc_path")
|
|
3038
|
-
});
|
|
3039
|
-
}
|
|
3040
|
-
function treeFromRow(row) {
|
|
3041
|
-
if (!isJsonObject(row)) return null;
|
|
3042
|
-
const treeId = readString2(row, "tree_id");
|
|
3043
|
-
const entryQuestionId = readString2(row, "entry_question_id");
|
|
3044
|
-
if (treeId === null || entryQuestionId === null) return null;
|
|
3045
|
-
const rawQuestions = Array.isArray(row["questions"]) ? row["questions"] : [];
|
|
3046
|
-
const questions = rawQuestions.map(questionFromRow).filter((question) => question !== null);
|
|
3047
|
-
if (!questions.some((question) => question.id === entryQuestionId)) return null;
|
|
3048
|
-
const rawPrinciples = Array.isArray(row["principles"]) ? row["principles"] : [];
|
|
3049
|
-
return Object.freeze({
|
|
3050
|
-
treeId,
|
|
3051
|
-
title: readString2(row, "title") ?? treeId,
|
|
3052
|
-
decides: readString2(row, "decides") ?? "",
|
|
3053
|
-
supplyWith: readString2(row, "supply_with") ?? "",
|
|
3054
|
-
entryQuestionId,
|
|
3055
|
-
principles: Object.freeze(
|
|
3056
|
-
rawPrinciples.map(principleFromRow).filter((p) => p !== null)
|
|
3057
|
-
),
|
|
3058
|
-
questions: Object.freeze(questions)
|
|
3059
|
-
});
|
|
3060
|
-
}
|
|
3061
|
-
function buildDecisionSpace(trees, source, digest) {
|
|
3062
|
-
const byId = new Map(trees.map((tree) => [tree.treeId, tree]));
|
|
3063
|
-
const space = {
|
|
3064
|
-
trees: Object.freeze([...trees].sort((a, b) => a.treeId < b.treeId ? -1 : 1)),
|
|
3065
|
-
source,
|
|
3066
|
-
digest,
|
|
3067
|
-
tree: (treeId) => byId.get(treeId) ?? null,
|
|
3068
|
-
question(treeId, questionId) {
|
|
3069
|
-
return byId.get(treeId)?.questions.find((q) => q.id === questionId) ?? null;
|
|
3070
|
-
},
|
|
3071
|
-
choices(treeId, questionId) {
|
|
3072
|
-
const question = space.question(treeId, questionId);
|
|
3073
|
-
return question === null ? Object.freeze([]) : question.options.map((option) => option.id);
|
|
3074
|
-
}
|
|
3075
|
-
};
|
|
3076
|
-
return Object.freeze(space);
|
|
3077
|
-
}
|
|
3078
|
-
function loadDecisionSpace(fs, path) {
|
|
3079
|
-
const outcome = validateContractFile(fs, path);
|
|
3080
|
-
if (outcome.kind !== "valid") return { kind: "contract_rejected", outcome };
|
|
3081
|
-
const document = outcome.projection[DECISION_TREES_DOCUMENT_KEY];
|
|
3082
|
-
if (!isJsonObject(document)) return { kind: "no_trees", path };
|
|
3083
|
-
const rows = document["trees"];
|
|
3084
|
-
if (!Array.isArray(rows)) return { kind: "no_trees", path };
|
|
3085
|
-
const trees = rows.map(treeFromRow).filter((tree) => tree !== null);
|
|
3086
|
-
if (trees.length === 0) return { kind: "no_trees", path };
|
|
3087
|
-
return { kind: "loaded", space: buildDecisionSpace(trees, outcome.path, outcome.digest.actual) };
|
|
3088
|
-
}
|
|
3089
|
-
function walkTree(tree, answerFor) {
|
|
3090
|
-
const path = [];
|
|
3091
|
-
const visited = /* @__PURE__ */ new Set();
|
|
3092
|
-
let questionId = tree.entryQuestionId;
|
|
3093
|
-
for (; ; ) {
|
|
3094
|
-
if (visited.has(questionId)) {
|
|
3095
|
-
return { kind: "unwalkable", detail: `the ${tree.treeId} tree loops at question "${questionId}"` };
|
|
3096
|
-
}
|
|
3097
|
-
visited.add(questionId);
|
|
3098
|
-
const question = tree.questions.find((candidate) => candidate.id === questionId);
|
|
3099
|
-
if (question === void 0) {
|
|
3100
|
-
return {
|
|
3101
|
-
kind: "unwalkable",
|
|
3102
|
-
detail: `the ${tree.treeId} tree points at a question "${questionId}" it does not carry`
|
|
3103
|
-
};
|
|
3104
|
-
}
|
|
3105
|
-
const implied = question.options.length === 1;
|
|
3106
|
-
const answer = implied ? question.options[0]?.id ?? null : answerFor(question.id);
|
|
3107
|
-
if (answer === null) return { kind: "unanswered", question };
|
|
3108
|
-
const option = question.options.find((candidate) => candidate.id === answer);
|
|
3109
|
-
if (option === void 0) return { kind: "unknown_option", question, given: answer };
|
|
3110
|
-
path.push({ questionId: question.id, optionId: option.id, implied });
|
|
3111
|
-
if (option.outcome.kind === "terminal") {
|
|
3112
|
-
return { kind: "terminal", outcome: option.outcome, path: Object.freeze(path) };
|
|
3113
|
-
}
|
|
3114
|
-
if (option.outcome.kind === "blocked") {
|
|
3115
|
-
return { kind: "blocked", outcome: option.outcome, path: Object.freeze(path) };
|
|
3116
|
-
}
|
|
3117
|
-
questionId = option.outcome.nextQuestionId;
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
function walkSingle(tree, answer) {
|
|
3121
|
-
return walkTree(tree, (questionId) => questionId === tree.entryQuestionId ? answer : null);
|
|
3122
|
-
}
|
|
3123
|
-
function stringAttribute(outcome, key) {
|
|
3124
|
-
const value = outcome.attributes[key];
|
|
3125
|
-
return typeof value === "string" ? value : null;
|
|
3126
|
-
}
|
|
3127
|
-
function listAttribute(outcome, key) {
|
|
3128
|
-
const value = outcome.attributes[key];
|
|
3129
|
-
return Array.isArray(value) ? value : Object.freeze([]);
|
|
3130
|
-
}
|
|
3131
|
-
function loadBundledDecisionSpace() {
|
|
3132
|
-
const doc = BUNDLED_CONTRACT;
|
|
3133
|
-
const trees = doc.projection?.[DECISION_TREES_DOCUMENT_KEY];
|
|
3134
|
-
const rows = Array.isArray(trees?.trees) ? trees.trees : [];
|
|
3135
|
-
const parsed = rows.map(treeFromRow).filter((t) => t !== null);
|
|
3136
|
-
if (parsed.length === 0) return { kind: "no_trees", path: "(bundled)" };
|
|
3137
|
-
return {
|
|
3138
|
-
kind: "loaded",
|
|
3139
|
-
// The digest travels inside the bundled document, so it is reported rather than recomputed.
|
|
3140
|
-
space: buildDecisionSpace(
|
|
3141
|
-
parsed,
|
|
3142
|
-
`(bundled with the Everfur CLI, generated ${bundledContractGeneratedAt()})`,
|
|
3143
|
-
bundledContractDigest()
|
|
3144
|
-
)
|
|
3145
|
-
};
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
// src/commands/plan/types.ts
|
|
3149
|
-
var PLAN_SCHEMA_VERSION = 1;
|
|
3150
|
-
var PLAN_DOCUMENT_KIND = "everfur.plan";
|
|
3151
|
-
var MISSING_FACT_NAMES = Object.freeze([
|
|
3152
|
-
"decisionTrees",
|
|
3153
|
-
"projectType",
|
|
3154
|
-
"packageManager",
|
|
3155
|
-
"capabilities",
|
|
3156
|
-
"identityMode",
|
|
3157
|
-
"sessionMintLanguage"
|
|
3158
|
-
]);
|
|
3159
|
-
|
|
3160
|
-
// src/commands/plan/capabilities.ts
|
|
3161
|
-
var NODE_SUBPATH_ATTRIBUTE = "node_subpath";
|
|
3162
|
-
var DART_LIBRARY_ATTRIBUTE = "dart_library";
|
|
3163
|
-
var NATIVE_PEERS_ATTRIBUTE = "native_peers";
|
|
3164
|
-
var SUMMARY_ATTRIBUTE = "summary";
|
|
3165
|
-
function resolveCapabilities(space, capabilities) {
|
|
3166
|
-
const tree = space.tree(CAPABILITIES_TREE);
|
|
3167
|
-
if (tree === null) {
|
|
3168
|
-
return Object.freeze({ mounted: [], blocked: [], unresolved: Object.freeze([...capabilities]) });
|
|
3169
|
-
}
|
|
3170
|
-
const mounted = [];
|
|
3171
|
-
const blocked = [];
|
|
3172
|
-
const unresolved = [];
|
|
3173
|
-
for (const capability of capabilities) {
|
|
3174
|
-
const result = walkSingle(tree, capability);
|
|
3175
|
-
if (result.kind === "terminal") {
|
|
3176
|
-
mounted.push({ capability, outcome: result.outcome });
|
|
3177
|
-
continue;
|
|
3178
|
-
}
|
|
3179
|
-
if (result.kind === "blocked") {
|
|
3180
|
-
blocked.push({
|
|
3181
|
-
capability,
|
|
3182
|
-
reason: result.outcome.reason,
|
|
3183
|
-
unblockOwner: result.outcome.unblockOwner,
|
|
3184
|
-
requestPath: result.outcome.requestPath,
|
|
3185
|
-
capabilityKeys: result.outcome.capabilityRefs
|
|
3186
|
-
});
|
|
3187
|
-
continue;
|
|
3188
|
-
}
|
|
3189
|
-
unresolved.push(capability);
|
|
3190
|
-
}
|
|
3191
|
-
return Object.freeze({
|
|
3192
|
-
mounted: Object.freeze(mounted),
|
|
3193
|
-
blocked: Object.freeze(blocked),
|
|
3194
|
-
unresolved: Object.freeze(unresolved)
|
|
3195
|
-
});
|
|
3196
|
-
}
|
|
3197
|
-
function nativePeersFor(mounted) {
|
|
3198
|
-
const peers = /* @__PURE__ */ new Set();
|
|
3199
|
-
for (const entry of mounted) {
|
|
3200
|
-
for (const peer of listAttribute(entry.outcome, NATIVE_PEERS_ATTRIBUTE)) peers.add(peer);
|
|
3201
|
-
}
|
|
3202
|
-
return Object.freeze([...peers].sort());
|
|
3203
|
-
}
|
|
3204
|
-
function importTargetFor(entry, isDart) {
|
|
3205
|
-
return stringAttribute(entry.outcome, isDart ? DART_LIBRARY_ATTRIBUTE : NODE_SUBPATH_ATTRIBUTE);
|
|
3206
|
-
}
|
|
3207
|
-
function summaryFor(entry) {
|
|
3208
|
-
return stringAttribute(entry.outcome, SUMMARY_ATTRIBUTE) ?? "";
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
// src/commands/plan/sdk-package.ts
|
|
3212
|
-
var SDK_PACKAGE = "@everfur/sdk";
|
|
3213
|
-
function sdkInstallSpec() {
|
|
3214
|
-
return SDK_PACKAGE ;
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
// src/commands/plan/codegen.ts
|
|
3218
|
-
var COMPONENT_SURFACES = Object.freeze({
|
|
3219
|
-
chat: "EverfurChat",
|
|
3220
|
-
animations: "PetStateView"
|
|
3221
|
-
});
|
|
3222
|
-
var HOOK_SURFACES = Object.freeze({
|
|
3223
|
-
records: "useEverfurRecords",
|
|
3224
|
-
photo: "usePhotoCheckup",
|
|
3225
|
-
video: "useEverfurVideo"
|
|
3226
|
-
});
|
|
3227
|
-
var HOOK_CALL_ARGS = Object.freeze({
|
|
3228
|
-
records: "{ petRef }",
|
|
3229
|
-
photo: "{ petRef }",
|
|
3230
|
-
video: ""
|
|
3231
|
-
});
|
|
3232
|
-
function subpathFor(capability) {
|
|
3233
|
-
return capability === "chat" ? SDK_PACKAGE : `${SDK_PACKAGE}/${capability}`;
|
|
3234
|
-
}
|
|
3235
|
-
function isComponentSurface(capability) {
|
|
3236
|
-
return capability in COMPONENT_SURFACES;
|
|
3237
|
-
}
|
|
3238
|
-
function hookFor(capability) {
|
|
3239
|
-
return HOOK_SURFACES[capability] ?? null;
|
|
3240
|
-
}
|
|
3241
|
-
function integrationFile(input) {
|
|
3242
|
-
const components = input.capabilities.filter(isComponentSurface);
|
|
3243
|
-
const hooks = input.capabilities.filter((c) => hookFor(c) !== null);
|
|
3244
|
-
if (components.length === 0 && hooks.length === 0) return null;
|
|
3245
|
-
const personalized = input.identityMode !== null && input.identityMode !== "anonymous";
|
|
3246
|
-
const rootNames = ["EverfurProvider", ...components.includes("chat") ? ["EverfurChat"] : []];
|
|
3247
|
-
if (personalized) rootNames.push("userRef");
|
|
3248
|
-
const imports = [`import { ${rootNames.join(", ")} } from '${SDK_PACKAGE}';`];
|
|
3249
|
-
for (const capability of components) {
|
|
3250
|
-
if (capability === "chat") continue;
|
|
3251
|
-
imports.push(`import { ${COMPONENT_SURFACES[capability]} } from '${subpathFor(capability)}';`);
|
|
3252
|
-
}
|
|
3253
|
-
for (const capability of hooks) {
|
|
3254
|
-
imports.push(`import { ${hookFor(capability)} } from '${subpathFor(capability)}';`);
|
|
3255
|
-
}
|
|
3256
|
-
const rendered = components.map((c) => ` <${COMPONENT_SURFACES[c]} />`).join("\n");
|
|
3257
|
-
const userProp = personalized ? `
|
|
3258
|
-
user={{
|
|
3259
|
-
userRef: userRef(YOUR_USER_ID),
|
|
3260
|
-
// Called whenever a session is needed. Must return a bearer minted by YOUR backend from the
|
|
3261
|
-
// secret key, which never ships in this app. Use an absolute URL: a relative path does not
|
|
3262
|
-
// resolve in a React Native bundle.
|
|
3263
|
-
getToken: () =>
|
|
3264
|
-
fetch('https://your-backend.example.com/everfur-session').then((r) => r.text()),
|
|
3265
|
-
}}` : "";
|
|
3266
|
-
const hookNote = hooks.length === 0 ? "" : `
|
|
3267
|
-
// ${hooks.map((c) => `${hookFor(c)}(${HOOK_CALL_ARGS[c] ?? ""}) drives the ${c} surface`).join(", ")}. These publish state, not a prebuilt screen, so the UI is yours: each returns
|
|
3268
|
-
// a status of loading, empty, error or populated. Render all four.
|
|
3269
|
-
`;
|
|
3270
|
-
const envNote = input.isExpo ? "In Expo, an EXPO_PUBLIC_ prefixed env var is inlined at build time and is a reasonable home for these." : "Read these from your own config. The publishable key is designed to ship in the bundle; the secret key is not.";
|
|
3271
|
-
const contents = `${imports.join("\n")}
|
|
3272
|
-
${hookNote}
|
|
3273
|
-
// Replace both. The publishable key is provisioned by Everfur and is safe to ship inside the app.
|
|
3274
|
-
// ${envNote}
|
|
3275
|
-
const EVERFUR_PUBLISHABLE_KEY = 'pk_live_replace_me';
|
|
3276
|
-
const EVERFUR_API_BASE_URL = 'https://api.everfur.com/api/v1';
|
|
3277
|
-
${personalized ? "const YOUR_USER_ID = 'replace-with-your-stable-user-id';\n" : ""}
|
|
3278
|
-
export default function App() {
|
|
3279
|
-
return (
|
|
3280
|
-
<EverfurProvider
|
|
3281
|
-
config={{
|
|
3282
|
-
publishableKey: EVERFUR_PUBLISHABLE_KEY,
|
|
3283
|
-
apiBaseUrl: EVERFUR_API_BASE_URL,
|
|
3284
|
-
}}${userProp}
|
|
3285
|
-
>
|
|
3286
|
-
${rendered}
|
|
3287
|
-
</EverfurProvider>
|
|
3288
|
-
);
|
|
3289
|
-
}
|
|
3290
|
-
`;
|
|
3291
|
-
return Object.freeze({ path: "App.tsx", language: "tsx", contents });
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
// src/commands/plan/planner.ts
|
|
3295
|
-
var PARTNER_SECRET_ENV_VAR = "EVERFUR_PARTNER_SECRET_KEY";
|
|
3296
|
-
function walkPlatform(space, detection) {
|
|
3297
|
-
const tree = space.tree(PLATFORM_TREE);
|
|
3298
|
-
if (tree === null) return null;
|
|
3299
|
-
return walkTree(tree, (questionId) => {
|
|
3300
|
-
if (questionId === PROJECT_SHAPE_QUESTION) {
|
|
3301
|
-
return detection.projectType === "unknown" ? null : detection.projectType;
|
|
3302
|
-
}
|
|
3303
|
-
if (questionId === PACKAGE_MANAGER_QUESTION) {
|
|
3304
|
-
return detection.packageManager === "undetermined" || detection.packageManager === "not-applicable" ? null : detection.packageManager;
|
|
3305
|
-
}
|
|
3306
|
-
return null;
|
|
3307
|
-
});
|
|
3308
|
-
}
|
|
3309
|
-
function walkIdentity(space, identityMode) {
|
|
3310
|
-
const tree = space.tree(IDENTITY_TREE);
|
|
3311
|
-
if (tree === null || identityMode === null) return null;
|
|
3312
|
-
return walkSingle(tree, identityMode);
|
|
3313
|
-
}
|
|
3314
|
-
function walkSessionMint(space, language) {
|
|
3315
|
-
const tree = space.tree(SESSION_MINT_TREE);
|
|
3316
|
-
if (tree === null || language === null) return null;
|
|
3317
|
-
return walkTree(
|
|
3318
|
-
tree,
|
|
3319
|
-
(questionId) => questionId === BACKEND_LANGUAGE_QUESTION ? language : null
|
|
3320
|
-
);
|
|
3321
|
-
}
|
|
3322
|
-
function requiresSessionMint(walk) {
|
|
3323
|
-
return walk?.kind === "terminal" && walk.outcome.thenAskTreeId === SESSION_MINT_TREE;
|
|
3324
|
-
}
|
|
3325
|
-
function terminalOf(walk) {
|
|
3326
|
-
return walk?.kind === "terminal" ? walk.outcome : null;
|
|
3327
|
-
}
|
|
3328
|
-
function supplyWithFor(space, treeId, questionId, fallback) {
|
|
3329
|
-
const question = space.question(treeId, questionId);
|
|
3330
|
-
if (question !== null && question.supplyWith !== null) return question.supplyWith;
|
|
3331
|
-
const tree = space.tree(treeId);
|
|
3332
|
-
if (tree !== null && tree.supplyWith.length > 0) return tree.supplyWith;
|
|
3333
|
-
return fallback;
|
|
3334
|
-
}
|
|
3335
|
-
function reasonFor(space, treeId, questionId, fallback) {
|
|
3336
|
-
const question = space.question(treeId, questionId);
|
|
3337
|
-
return question !== null && question.whyItIsAsked.length > 0 ? question.whyItIsAsked : fallback;
|
|
3338
|
-
}
|
|
3339
|
-
function detectionReason(detection, kind) {
|
|
3340
|
-
if (kind === "projectType") {
|
|
3341
|
-
return detection.projectTypeEvidence.length > 0 ? `could not identify the project shape at ${detection.root}: ${detection.projectTypeEvidence.join("; ")}` : `could not identify the project shape at ${detection.root}`;
|
|
3342
|
-
}
|
|
3343
|
-
return detection.packageManagerEvidence.length > 0 ? `could not identify the package manager at ${detection.root}: ${detection.packageManagerEvidence.join("; ")}` : `could not identify the package manager at ${detection.root}`;
|
|
3344
|
-
}
|
|
3345
|
-
function missingFactsFor(input) {
|
|
3346
|
-
const missing = [];
|
|
3347
|
-
const detection = input.detection;
|
|
3348
|
-
const space = input.space ?? null;
|
|
3349
|
-
if (space === null) {
|
|
3350
|
-
missing.push({
|
|
3351
|
-
fact: "decisionTrees",
|
|
3352
|
-
reason: "no contract-public projection was found, so the decision space this plan is built from is unavailable; the CLI does not carry a copy of it",
|
|
3353
|
-
supplyWith: "pass --contract <file>, or export EVERFUR_CONTRACT_PUBLIC"
|
|
3354
|
-
});
|
|
3355
|
-
if (detection.projectType === "unknown") {
|
|
3356
|
-
missing.push({
|
|
3357
|
-
fact: "projectType",
|
|
3358
|
-
reason: detectionReason(detection, "projectType"),
|
|
3359
|
-
supplyWith: "run from the application root, or pass --cwd <path> pointing at it"
|
|
3360
|
-
});
|
|
3361
|
-
}
|
|
3362
|
-
if (detection.packageManager === "undetermined") {
|
|
3363
|
-
missing.push({
|
|
3364
|
-
fact: "packageManager",
|
|
3365
|
-
reason: detectionReason(detection, "packageManager"),
|
|
3366
|
-
supplyWith: 'add a "packageManager" field to package.json, or leave exactly one lockfile in the project root'
|
|
3367
|
-
});
|
|
3368
|
-
}
|
|
3369
|
-
if (input.capabilities === null || input.capabilities.length === 0) {
|
|
3370
|
-
missing.push({
|
|
3371
|
-
fact: "capabilities",
|
|
3372
|
-
reason: "which Everfur capabilities this integration uses cannot be read from the project",
|
|
3373
|
-
supplyWith: "--capabilities <list>; the valid ids are published in the contract"
|
|
3374
|
-
});
|
|
3375
|
-
}
|
|
3376
|
-
if (input.identityMode === null) {
|
|
3377
|
-
missing.push({
|
|
3378
|
-
fact: "identityMode",
|
|
3379
|
-
reason: "how this integration identifies end users cannot be read from the project",
|
|
3380
|
-
supplyWith: "--identity <mode>; the valid modes are published in the contract"
|
|
3381
|
-
});
|
|
3382
|
-
}
|
|
3383
|
-
return Object.freeze(missing);
|
|
3384
|
-
}
|
|
3385
|
-
const platform = walkPlatform(space, detection);
|
|
3386
|
-
const drift = platform !== null && platform.kind === "unknown_option" ? platform : null;
|
|
3387
|
-
if (detection.projectType === "unknown" || drift?.question.id === PROJECT_SHAPE_QUESTION) {
|
|
3388
|
-
missing.push({
|
|
3389
|
-
fact: "projectType",
|
|
3390
|
-
reason: drift?.question.id === PROJECT_SHAPE_QUESTION ? `this project reads as "${drift.given}", which the platform decision tree at ${space.source} does not carry` : detectionReason(detection, "projectType"),
|
|
3391
|
-
supplyWith: supplyWithFor(
|
|
3392
|
-
space,
|
|
3393
|
-
PLATFORM_TREE,
|
|
3394
|
-
PROJECT_SHAPE_QUESTION,
|
|
3395
|
-
"run from the application root, or pass --cwd <path> pointing at it"
|
|
3396
|
-
)
|
|
3397
|
-
});
|
|
3398
|
-
}
|
|
3399
|
-
if (detection.packageManager === "undetermined" || drift?.question.id === PACKAGE_MANAGER_QUESTION) {
|
|
3400
|
-
missing.push({
|
|
3401
|
-
fact: "packageManager",
|
|
3402
|
-
reason: drift?.question.id === PACKAGE_MANAGER_QUESTION ? `this project installs with "${drift.given}", which the platform decision tree at ${space.source} does not carry` : detectionReason(detection, "packageManager"),
|
|
3403
|
-
supplyWith: supplyWithFor(
|
|
3404
|
-
space,
|
|
3405
|
-
PLATFORM_TREE,
|
|
3406
|
-
PACKAGE_MANAGER_QUESTION,
|
|
3407
|
-
'add a "packageManager" field to package.json, or leave exactly one lockfile in the project root'
|
|
3408
|
-
)
|
|
3409
|
-
});
|
|
3410
|
-
}
|
|
3411
|
-
if (input.capabilities === null || input.capabilities.length === 0) {
|
|
3412
|
-
missing.push({
|
|
3413
|
-
fact: "capabilities",
|
|
3414
|
-
reason: reasonFor(
|
|
3415
|
-
space,
|
|
3416
|
-
CAPABILITIES_TREE,
|
|
3417
|
-
CAPABILITY_QUESTION,
|
|
3418
|
-
"which Everfur capabilities this integration uses cannot be read from the project"
|
|
3419
|
-
),
|
|
3420
|
-
supplyWith: supplyWithFor(space, CAPABILITIES_TREE, CAPABILITY_QUESTION, "--capabilities <list>")
|
|
3421
|
-
});
|
|
3422
|
-
} else {
|
|
3423
|
-
const resolution = resolveCapabilities(space, input.capabilities);
|
|
3424
|
-
if (resolution.unresolved.length > 0) {
|
|
3425
|
-
missing.push({
|
|
3426
|
-
fact: "capabilities",
|
|
3427
|
-
reason: `the decision space at ${space.source} carries no outcome for ${resolution.unresolved.join(", ")}`,
|
|
3428
|
-
supplyWith: supplyWithFor(space, CAPABILITIES_TREE, CAPABILITY_QUESTION, "--capabilities <list>")
|
|
3429
|
-
});
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
if (input.identityMode === null) {
|
|
3433
|
-
missing.push({
|
|
3434
|
-
fact: "identityMode",
|
|
3435
|
-
reason: reasonFor(
|
|
3436
|
-
space,
|
|
3437
|
-
IDENTITY_TREE,
|
|
3438
|
-
IDENTITY_QUESTION,
|
|
3439
|
-
"how this integration identifies end users cannot be read from the project"
|
|
3440
|
-
),
|
|
3441
|
-
supplyWith: supplyWithFor(space, IDENTITY_TREE, IDENTITY_QUESTION, "--identity <mode>")
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3444
|
-
const identity = space === null ? null : walkIdentity(space, input.identityMode);
|
|
3445
|
-
if (requiresSessionMint(identity) && input.sessionMintLanguage === null) {
|
|
3446
|
-
missing.push({
|
|
3447
|
-
fact: "sessionMintLanguage",
|
|
3448
|
-
reason: reasonFor(
|
|
3449
|
-
space,
|
|
3450
|
-
SESSION_MINT_TREE,
|
|
3451
|
-
BACKEND_LANGUAGE_QUESTION,
|
|
3452
|
-
"this identity mode mints sessions on your backend, and the language cannot be read from this project"
|
|
3453
|
-
),
|
|
3454
|
-
supplyWith: supplyWithFor(space, SESSION_MINT_TREE, BACKEND_LANGUAGE_QUESTION, "--backend <language>")
|
|
3455
|
-
});
|
|
3456
|
-
}
|
|
3457
|
-
return Object.freeze(missing);
|
|
3458
|
-
}
|
|
3459
|
-
function toPlanDetection(detection) {
|
|
3460
|
-
return {
|
|
3461
|
-
root: detection.root,
|
|
3462
|
-
projectType: detection.projectType,
|
|
3463
|
-
projectTypeEvidence: detection.projectTypeEvidence,
|
|
3464
|
-
packageManager: detection.packageManager,
|
|
3465
|
-
packageManagerEvidence: detection.packageManagerEvidence,
|
|
3466
|
-
dependencies: detection.dependencies.map((dependency) => ({
|
|
3467
|
-
name: dependency.name,
|
|
3468
|
-
declared: dependency.declared,
|
|
3469
|
-
installed: dependency.installed,
|
|
3470
|
-
source: dependency.source,
|
|
3471
|
-
scope: dependency.scope
|
|
3472
|
-
}))
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
function principlesFor(space, consulted) {
|
|
3476
|
-
if (space === null) return Object.freeze([]);
|
|
3477
|
-
const principles = [];
|
|
3478
|
-
for (const treeId of [...consulted].sort()) {
|
|
3479
|
-
const tree = space.tree(treeId);
|
|
3480
|
-
if (tree === null) continue;
|
|
3481
|
-
for (const principle of tree.principles) {
|
|
3482
|
-
principles.push({
|
|
3483
|
-
treeId,
|
|
3484
|
-
id: principle.id,
|
|
3485
|
-
title: principle.title,
|
|
3486
|
-
statement: principle.statement,
|
|
3487
|
-
docPath: principle.docPath
|
|
3488
|
-
});
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
return Object.freeze(principles);
|
|
3492
|
-
}
|
|
3493
|
-
function warningsFor(input, mounted, blocked) {
|
|
3494
|
-
const warnings = [];
|
|
3495
|
-
const { detection } = input;
|
|
3496
|
-
if (blocked.length > 0 && mounted.length > 0) {
|
|
3497
|
-
warnings.push({
|
|
3498
|
-
code: "capabilities_blocked",
|
|
3499
|
-
message: `This plan covers ${mounted.length} of the ${mounted.length + blocked.length} capabilities you named. The rest cannot be built today; see the blocked section for the reason and where to ask.`
|
|
3500
|
-
});
|
|
3501
|
-
}
|
|
3502
|
-
const peers = nativePeersFor(mounted);
|
|
3503
|
-
if (detection.projectType === "expo-managed" && peers.length > 0) {
|
|
3504
|
-
warnings.push({
|
|
3505
|
-
code: "expo_go_incompatible",
|
|
3506
|
-
message: `The selected capabilities need native modules (${peers.join(", ")}), which Expo Go cannot load. This project needs a development build.`
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
const nodeProject = detection.projectType === "expo-managed" || detection.projectType === "bare-react-native" || detection.projectType === "web";
|
|
3510
|
-
if (nodeProject && detection.dependencies.length > 0) {
|
|
3511
|
-
const anyInstalled = detection.dependencies.some((dependency) => dependency.installed !== null);
|
|
3512
|
-
if (!anyInstalled) {
|
|
3513
|
-
warnings.push({
|
|
3514
|
-
code: "dependencies_not_installed",
|
|
3515
|
-
message: "No resolved versions were found under node_modules, so every reported version is the declared range from package.json rather than what is actually installed."
|
|
3516
|
-
});
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
const hasSdk = detection.dependencies.some(
|
|
3520
|
-
(dependency) => dependency.name === SDK_PACKAGE || dependency.name === "everfur_sdk"
|
|
3521
|
-
);
|
|
3522
|
-
if (!hasSdk) {
|
|
3523
|
-
warnings.push({
|
|
3524
|
-
code: "sdk_not_present",
|
|
3525
|
-
message: "The Everfur SDK is not declared in this project yet, so this integration starts from installing it."
|
|
3526
|
-
});
|
|
3527
|
-
}
|
|
3528
|
-
return Object.freeze(warnings);
|
|
3529
|
-
}
|
|
3530
|
-
function installSteps(detection, platform, mounted) {
|
|
3531
|
-
const steps = [];
|
|
3532
|
-
const commands = listAttribute(platform, "install_commands");
|
|
3533
|
-
const verb = stringAttribute(platform, "install_verb");
|
|
3534
|
-
const manifest = stringAttribute(platform, "manifest") ?? "package.json";
|
|
3535
|
-
steps.push({
|
|
3536
|
-
id: "install-sdk",
|
|
3537
|
-
phase: "install",
|
|
3538
|
-
title: "Add the Everfur SDK",
|
|
3539
|
-
detail: "Installs the package that carries every capability surface used below.",
|
|
3540
|
-
// Only the SPEC changes while the package is unpublished. The verb the platform tree
|
|
3541
|
-
// published still decides the command, and a platform naming its own install_commands still
|
|
3542
|
-
// wins outright, so nothing here overrides a more specific answer.
|
|
3543
|
-
commands: commands.length > 0 ? Object.freeze([...commands]) : verb === null ? Object.freeze([]) : Object.freeze([`${verb} ${sdkInstallSpec()}`]),
|
|
3544
|
-
files: [manifest]
|
|
3545
|
-
});
|
|
3546
|
-
const peers = nativePeersFor(mounted);
|
|
3547
|
-
if (peers.length === 0) return Object.freeze(steps);
|
|
3548
|
-
const peerInstall = detection.projectType === "expo-managed" ? `npx expo install ${peers.join(" ")}` : verb === null ? null : `${verb} ${peers.join(" ")}`;
|
|
3549
|
-
steps.push({
|
|
3550
|
-
id: "install-native-peers",
|
|
3551
|
-
phase: "install",
|
|
3552
|
-
title: "Add the optional native peer dependencies",
|
|
3553
|
-
detail: `The selected capabilities bind these modules at composition time: ${peers.join(", ")}. They are declared optional by ${SDK_PACKAGE}, so nothing installs them for you.`,
|
|
3554
|
-
commands: peerInstall === null ? Object.freeze([]) : Object.freeze([peerInstall]),
|
|
3555
|
-
files: [manifest]
|
|
3556
|
-
});
|
|
3557
|
-
return Object.freeze(steps);
|
|
3558
|
-
}
|
|
3559
|
-
function nativeSteps(detection, mounted) {
|
|
3560
|
-
if (nativePeersFor(mounted).length === 0) return Object.freeze([]);
|
|
3561
|
-
if (detection.projectType === "expo-managed") {
|
|
3562
|
-
return Object.freeze([
|
|
3563
|
-
{
|
|
3564
|
-
id: "native-development-build",
|
|
3565
|
-
phase: "native",
|
|
3566
|
-
title: "Create a development build",
|
|
3567
|
-
detail: "The peer modules above ship native code, which Expo Go cannot load. Generate the native projects and install a development build on the device or simulator you test on.",
|
|
3568
|
-
commands: ["npx expo prebuild", "npx expo run:ios", "npx expo run:android"],
|
|
3569
|
-
files: ["app.json"]
|
|
3570
|
-
}
|
|
3571
|
-
]);
|
|
3572
|
-
}
|
|
3573
|
-
if (detection.projectType === "bare-react-native") {
|
|
3574
|
-
return Object.freeze([
|
|
3575
|
-
{
|
|
3576
|
-
id: "native-pod-install",
|
|
3577
|
-
phase: "native",
|
|
3578
|
-
title: "Link the native modules on iOS",
|
|
3579
|
-
detail: "Autolinking picks up the new pods once the Podfile is resolved again.",
|
|
3580
|
-
commands: ["cd ios && pod install"],
|
|
3581
|
-
files: ["ios/Podfile.lock"]
|
|
3582
|
-
}
|
|
3583
|
-
]);
|
|
3584
|
-
}
|
|
3585
|
-
return Object.freeze([]);
|
|
3586
|
-
}
|
|
3587
|
-
function identityStep(detection, identityMode, outcome) {
|
|
3588
|
-
const userRefHeader = stringAttribute(outcome, "user_ref_header");
|
|
3589
|
-
const roleHeader = stringAttribute(outcome, "user_role_header");
|
|
3590
|
-
if (userRefHeader === null) {
|
|
3591
|
-
return {
|
|
3592
|
-
id: "configure-identity-anonymous",
|
|
3593
|
-
phase: "configure",
|
|
3594
|
-
title: "Run without an end user reference",
|
|
3595
|
-
detail: [
|
|
3596
|
-
"Send no end user reference header.",
|
|
3597
|
-
"The API fences ownership on the (user, pet) composite, so with no user reference asserted, nothing owned by a specific end user is reachable.",
|
|
3598
|
-
"Choose this only for a surface that shows no per-user data."
|
|
3599
|
-
].join(" "),
|
|
3600
|
-
commands: [],
|
|
3601
|
-
files: []
|
|
3602
|
-
};
|
|
3603
|
-
}
|
|
3604
|
-
return {
|
|
3605
|
-
id: `configure-identity-${identityMode}`,
|
|
3606
|
-
phase: "configure",
|
|
3607
|
-
title: "Assert the end user on every request",
|
|
3608
|
-
detail: [
|
|
3609
|
-
`Assert the end user on each request with the ${userRefHeader} header${roleHeader === null ? "" : `, and their role with ${roleHeader} when your integration distinguishes roles`}.`,
|
|
3610
|
-
"The API fences ownership on the (user, pet) composite, so the user reference is what makes the pets of one tenant invisible to another. Send a stable, non-guessable value per end user.",
|
|
3611
|
-
"It is case sensitive and it is compared exactly: normalise your identifiers once where they are minted, never on this path."
|
|
3612
|
-
].join(" "),
|
|
3613
|
-
commands: [],
|
|
3614
|
-
files: detection.projectType === "flutter" ? ["lib/everfur_client.dart"] : ["app/everfur-provider.ts"]
|
|
3615
|
-
};
|
|
3616
|
-
}
|
|
3617
|
-
function sessionMintStep(detection, outcome) {
|
|
3618
|
-
const subpath = stringAttribute(outcome, "import_subpath");
|
|
3619
|
-
const route = stringAttribute(outcome, "route");
|
|
3620
|
-
const secretHeader = stringAttribute(outcome, "secret_header");
|
|
3621
|
-
const publishableHeader = stringAttribute(outcome, "publishable_header");
|
|
3622
|
-
const bodyFields = listAttribute(outcome, "body_fields");
|
|
3623
|
-
const detail = [
|
|
3624
|
-
`Read the partner secret key from the ${PARTNER_SECRET_ENV_VAR} environment variable on your server and exchange it for a session there.`
|
|
3625
|
-
];
|
|
3626
|
-
if (subpath !== null) {
|
|
3627
|
-
detail.push(
|
|
3628
|
-
`Import the minter from ${subpath} and from nowhere else: that subpath declares no react-native condition, so a mobile bundler cannot resolve it, and the subpath is the fence rather than the function.`
|
|
3629
|
-
);
|
|
3630
|
-
}
|
|
3631
|
-
if (route !== null) {
|
|
3632
|
-
detail.push(
|
|
3633
|
-
`There is no first-party minter for this language, so call ${route} from your backend directly${secretHeader === null || publishableHeader === null ? "" : `, sending the ${secretHeader} and ${publishableHeader} headers`}${bodyFields.length === 0 ? "" : ` and a body carrying ${bodyFields.join(" and ")}`}.`
|
|
3634
|
-
);
|
|
3635
|
-
}
|
|
3636
|
-
detail.push(
|
|
3637
|
-
"The secret key must never reach a client bundle, a mobile binary, or a browser: anything shipped to a device is readable by the person holding it.",
|
|
3638
|
-
"This CLI does not mint, read, or print that key. Obtain it through the partner console."
|
|
3639
|
-
);
|
|
3640
|
-
return {
|
|
3641
|
-
id: "configure-session-mint",
|
|
3642
|
-
phase: "configure",
|
|
3643
|
-
title: "Mint sessions on your backend",
|
|
3644
|
-
detail: detail.join(" "),
|
|
3645
|
-
commands: [],
|
|
3646
|
-
files: detection.projectType === "flutter" ? ["lib/everfur_session.dart"] : ["server/everfur-session.ts"]
|
|
3647
|
-
};
|
|
3648
|
-
}
|
|
3649
|
-
function codeSteps(detection, mounted) {
|
|
3650
|
-
const isDart = detection.projectType === "flutter";
|
|
3651
|
-
return Object.freeze(
|
|
3652
|
-
mounted.map((entry) => {
|
|
3653
|
-
const importTarget = importTargetFor(entry, isDart);
|
|
3654
|
-
const summary = summaryFor(entry);
|
|
3655
|
-
const hook = hookFor(entry.capability);
|
|
3656
|
-
return {
|
|
3657
|
-
id: `mount-${entry.capability}`,
|
|
3658
|
-
phase: "code",
|
|
3659
|
-
title: `Mount the ${entry.capability} capability`,
|
|
3660
|
-
detail: importTarget === null ? `${summary} The contract publishes no import target for this project shape.` : hook !== null ? `${summary} Import ${hook} from ${importTarget}. This surface publishes state, not a prebuilt screen, so the UI is yours: it reports loading, empty, error and populated, and all four need rendering.` : `${summary} Import from ${importTarget} and mount it inside the Everfur provider you configured above.`,
|
|
3661
|
-
commands: [],
|
|
3662
|
-
files: []
|
|
3663
|
-
};
|
|
3664
|
-
})
|
|
3665
|
-
);
|
|
3666
|
-
}
|
|
3667
|
-
function verifyStep() {
|
|
3668
|
-
return {
|
|
3669
|
-
id: "verify-request-id",
|
|
3670
|
-
phase: "verify",
|
|
3671
|
-
title: "Confirm the request id round trip",
|
|
3672
|
-
detail: "Send one request through your integration and record the X-Request-ID value. The API echoes a caller-supplied id and generates one otherwise; that id is what support correlates against. Capture it in your logs before you need it.",
|
|
3673
|
-
commands: [],
|
|
3674
|
-
files: []
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
function consultedTrees(space, mintRequired) {
|
|
3678
|
-
if (space === null) return Object.freeze([]);
|
|
3679
|
-
const ids = [PLATFORM_TREE, CAPABILITIES_TREE, IDENTITY_TREE];
|
|
3680
|
-
if (mintRequired) ids.push(SESSION_MINT_TREE);
|
|
3681
|
-
return Object.freeze(ids.filter((id) => space.tree(id) !== null));
|
|
3682
|
-
}
|
|
3683
|
-
function provenanceFor(space) {
|
|
3684
|
-
return space === null ? null : {
|
|
3685
|
-
source: space.source,
|
|
3686
|
-
digest: space.digest,
|
|
3687
|
-
trees: Object.freeze(space.trees.map((tree) => tree.treeId))
|
|
3688
|
-
};
|
|
3689
|
-
}
|
|
3690
|
-
function resolutionFor(space, capabilities) {
|
|
3691
|
-
if (space === null || capabilities === null || capabilities.length === 0) {
|
|
3692
|
-
return { mounted: Object.freeze([]), blocked: Object.freeze([]), unresolved: Object.freeze([]) };
|
|
3693
|
-
}
|
|
3694
|
-
return resolveCapabilities(space, capabilities);
|
|
3695
|
-
}
|
|
3696
|
-
function planIntegration(input) {
|
|
3697
|
-
const space = input.space ?? null;
|
|
3698
|
-
const missingFacts = missingFactsFor(input);
|
|
3699
|
-
const detected = toPlanDetection(input.detection);
|
|
3700
|
-
const generatedBy = { name: "@everfur/cli", version: input.cliVersion };
|
|
3701
|
-
const identity = space === null ? null : walkIdentity(space, input.identityMode);
|
|
3702
|
-
const mintRequired = requiresSessionMint(identity);
|
|
3703
|
-
const resolution = resolutionFor(space, input.capabilities);
|
|
3704
|
-
const principles = principlesFor(space, consultedTrees(space, mintRequired));
|
|
3705
|
-
const decisionSpace = provenanceFor(space);
|
|
3706
|
-
const supplied = {
|
|
3707
|
-
capabilities: input.capabilities,
|
|
3708
|
-
identityMode: input.identityMode,
|
|
3709
|
-
sessionMintLanguage: input.sessionMintLanguage
|
|
3710
|
-
};
|
|
3711
|
-
if (missingFacts.length > 0) {
|
|
3712
|
-
return Object.freeze({
|
|
3713
|
-
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
3714
|
-
kind: PLAN_DOCUMENT_KIND,
|
|
3715
|
-
generatedBy,
|
|
3716
|
-
status: "underspecified",
|
|
3717
|
-
detected,
|
|
3718
|
-
supplied,
|
|
3719
|
-
missingFacts,
|
|
3720
|
-
steps: Object.freeze([]),
|
|
3721
|
-
blocked: resolution.blocked,
|
|
3722
|
-
principles,
|
|
3723
|
-
warnings: warningsFor(input, resolution.mounted, resolution.blocked),
|
|
3724
|
-
decisionSpace
|
|
3725
|
-
});
|
|
3726
|
-
}
|
|
3727
|
-
const warnings = warningsFor(input, resolution.mounted, resolution.blocked);
|
|
3728
|
-
if (resolution.mounted.length === 0) {
|
|
3729
|
-
return Object.freeze({
|
|
3730
|
-
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
3731
|
-
kind: PLAN_DOCUMENT_KIND,
|
|
3732
|
-
generatedBy,
|
|
3733
|
-
status: "blocked",
|
|
3734
|
-
detected,
|
|
3735
|
-
supplied,
|
|
3736
|
-
missingFacts: Object.freeze([]),
|
|
3737
|
-
steps: Object.freeze([]),
|
|
3738
|
-
blocked: resolution.blocked,
|
|
3739
|
-
principles,
|
|
3740
|
-
warnings,
|
|
3741
|
-
decisionSpace
|
|
3742
|
-
});
|
|
3743
|
-
}
|
|
3744
|
-
const platformTerminal = space === null ? null : terminalOf(walkPlatform(space, input.detection));
|
|
3745
|
-
const identityTerminal = terminalOf(identity);
|
|
3746
|
-
const mintTerminal = mintRequired && space !== null ? terminalOf(walkSessionMint(space, input.sessionMintLanguage)) : null;
|
|
3747
|
-
const steps = [];
|
|
3748
|
-
if (platformTerminal !== null) {
|
|
3749
|
-
steps.push(...installSteps(input.detection, platformTerminal, resolution.mounted));
|
|
3750
|
-
}
|
|
3751
|
-
steps.push(...nativeSteps(input.detection, resolution.mounted));
|
|
3752
|
-
if (identityTerminal !== null && input.identityMode !== null) {
|
|
3753
|
-
steps.push(identityStep(input.detection, input.identityMode, identityTerminal));
|
|
3754
|
-
}
|
|
3755
|
-
if (mintTerminal !== null) {
|
|
3756
|
-
steps.push(sessionMintStep(input.detection, mintTerminal));
|
|
3757
|
-
}
|
|
3758
|
-
steps.push(...codeSteps(input.detection, resolution.mounted));
|
|
3759
|
-
const file = integrationFile({
|
|
3760
|
-
capabilities: resolution.mounted.map((entry) => entry.capability),
|
|
3761
|
-
identityMode: input.identityMode,
|
|
3762
|
-
isExpo: input.detection.projectType === "expo-managed"
|
|
3763
|
-
});
|
|
3764
|
-
if (file !== null) {
|
|
3765
|
-
steps.push({
|
|
3766
|
-
id: "write-integration",
|
|
3767
|
-
phase: "code",
|
|
3768
|
-
title: `Write ${file.path}`,
|
|
3769
|
-
detail: "The complete provider file for the capabilities and identity mode you chose. Write it, then replace the two constants at the top with the publishable key and base URL you were given.",
|
|
3770
|
-
commands: [],
|
|
3771
|
-
files: [file.path],
|
|
3772
|
-
code: [file]
|
|
3773
|
-
});
|
|
3774
|
-
}
|
|
3775
|
-
steps.push(verifyStep());
|
|
3776
|
-
return Object.freeze({
|
|
3777
|
-
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
3778
|
-
kind: PLAN_DOCUMENT_KIND,
|
|
3779
|
-
generatedBy,
|
|
3780
|
-
status: "ready",
|
|
3781
|
-
detected,
|
|
3782
|
-
// Echoes what was ASKED FOR, not what survived. A caller who named a blocked
|
|
3783
|
-
// capability has to see it here and in `blocked`, or the document reads as
|
|
3784
|
-
// though they never asked.
|
|
3785
|
-
supplied,
|
|
3786
|
-
missingFacts: Object.freeze([]),
|
|
3787
|
-
steps: Object.freeze(steps),
|
|
3788
|
-
blocked: resolution.blocked,
|
|
3789
|
-
principles,
|
|
3790
|
-
warnings,
|
|
3791
|
-
decisionSpace
|
|
3792
|
-
});
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
// src/commands/verify/steps.ts
|
|
3796
|
-
var SESSIONS_PATH = "/widget/v1/sessions";
|
|
3797
|
-
var ENTITLEMENTS_PATH = "/widget/v1/entitlements";
|
|
3798
|
-
function step(id, title, result, reason, requestId = null, details = []) {
|
|
3799
|
-
return Object.freeze({ id, title, result, reason, requestId, details: Object.freeze([...details]) });
|
|
3800
|
-
}
|
|
3801
|
-
async function mintSession(options) {
|
|
3802
|
-
const title = `mint a session for ${options.userRef}`;
|
|
3803
|
-
const secret = resolveCredential(options.env, PARTNER_SECRET_KEY);
|
|
3804
|
-
const publishable = resolveCredential(options.env, PARTNER_KEY);
|
|
3805
|
-
const absent = [secret, publishable].filter((entry) => entry.status === "absent");
|
|
3806
|
-
if (absent.length > 0) {
|
|
3807
|
-
return {
|
|
3808
|
-
bearer: null,
|
|
3809
|
-
step: step(
|
|
3810
|
-
"mint-session",
|
|
3811
|
-
title,
|
|
3812
|
-
"SKIP",
|
|
3813
|
-
`the mint needs both keys and ${absent.map((entry) => entry.spec.envVar).join(" and ")} ${absent.length === 1 ? "is" : "are"} not set`,
|
|
3814
|
-
null,
|
|
3815
|
-
absent.map((entry) => `export ${entry.spec.envVar}`)
|
|
3816
|
-
)
|
|
3817
|
-
};
|
|
3818
|
-
}
|
|
3819
|
-
const malformed = [secret, publishable].filter((entry) => entry.status === "malformed");
|
|
3820
|
-
if (malformed.length > 0 || secret.credential === null || publishable.credential === null) {
|
|
3821
|
-
return {
|
|
3822
|
-
bearer: null,
|
|
3823
|
-
step: step(
|
|
3824
|
-
"mint-session",
|
|
3825
|
-
title,
|
|
3826
|
-
"FAIL",
|
|
3827
|
-
"a configured key has the wrong shape, so the mint was not attempted",
|
|
3828
|
-
null,
|
|
3829
|
-
malformed.map((entry) => entry.reason ?? `${entry.spec.envVar} is malformed`)
|
|
3830
|
-
)
|
|
3831
|
-
};
|
|
3832
|
-
}
|
|
3833
|
-
const result = await options.client.request({
|
|
3834
|
-
method: "POST",
|
|
3835
|
-
path: SESSIONS_PATH,
|
|
3836
|
-
credentials: {
|
|
3837
|
-
"x-everfur-partner-secret-key": secret.credential,
|
|
3838
|
-
"x-everfur-partner-key": publishable.credential
|
|
3839
|
-
},
|
|
3840
|
-
body: { user_ref: options.userRef }
|
|
3841
|
-
});
|
|
3842
|
-
if (!result.ok) {
|
|
3843
|
-
const hint = result.kind === "http" && result.status === 401 ? "the mint returns one opaque 401 for every credential failure: wrong key, wrong tenant, inactive client, or chat disabled" : "the session mint is the first thing a partner backend does; nothing downstream can work until it succeeds";
|
|
3844
|
-
return {
|
|
3845
|
-
bearer: null,
|
|
3846
|
-
step: step("mint-session", title, "FAIL", describeFailure(result), result.requestId, [hint])
|
|
3847
|
-
};
|
|
3848
|
-
}
|
|
3849
|
-
const body = result.value;
|
|
3850
|
-
const token = isJsonObject(body) ? body["session_token"] : void 0;
|
|
3851
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
3852
|
-
return {
|
|
3853
|
-
bearer: null,
|
|
3854
|
-
step: step(
|
|
3855
|
-
"mint-session",
|
|
3856
|
-
title,
|
|
3857
|
-
"FAIL",
|
|
3858
|
-
`the mint answered ${result.status} without a session_token`,
|
|
3859
|
-
result.requestId,
|
|
3860
|
-
["check that --base-url points at the Everfur API"]
|
|
3861
|
-
)
|
|
3862
|
-
};
|
|
3863
|
-
}
|
|
3864
|
-
const expiresAt = isJsonObject(body) && typeof body["expires_at"] === "string" ? body["expires_at"] : "unreported";
|
|
3865
|
-
const advisory = isJsonObject(body) && Array.isArray(body["capabilities"]) ? body["capabilities"].filter((entry) => typeof entry === "string") : [];
|
|
3866
|
-
return {
|
|
3867
|
-
bearer: withPrefix(ephemeralCredential("minted session token", token), "Bearer "),
|
|
3868
|
-
step: step(
|
|
3869
|
-
"mint-session",
|
|
3870
|
-
title,
|
|
3871
|
-
"PASS",
|
|
3872
|
-
`minted a session valid until ${expiresAt}`,
|
|
3873
|
-
result.requestId,
|
|
3874
|
-
[
|
|
3875
|
-
`advisory first-paint capabilities: ${advisory.length === 0 ? "(none)" : advisory.join(", ")}`,
|
|
3876
|
-
"advisory only; the authoritative answer is the entitlement verdict below",
|
|
3877
|
-
"the token itself is never printed"
|
|
3878
|
-
]
|
|
3879
|
-
)
|
|
3880
|
-
};
|
|
3881
|
-
}
|
|
3882
|
-
async function pollEntitlements(options, bearer) {
|
|
3883
|
-
const title = "read the entitlement verdict";
|
|
3884
|
-
if (bearer === null) {
|
|
3885
|
-
return {
|
|
3886
|
-
capabilities: null,
|
|
3887
|
-
step: step("poll-entitlements", title, "SKIP", "no session was minted, so there is no bearer to read a verdict with")
|
|
3888
|
-
};
|
|
3889
|
-
}
|
|
3890
|
-
let lastRequestId = null;
|
|
3891
|
-
let observed = [];
|
|
3892
|
-
const attempts = Math.max(1, options.attempts);
|
|
3893
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
3894
|
-
const result = await options.client.request({
|
|
3895
|
-
method: "GET",
|
|
3896
|
-
path: ENTITLEMENTS_PATH,
|
|
3897
|
-
credentials: { authorization: bearer }
|
|
3898
|
-
});
|
|
3899
|
-
if (!result.ok) {
|
|
3900
|
-
return {
|
|
3901
|
-
capabilities: null,
|
|
3902
|
-
step: step("poll-entitlements", title, "FAIL", describeFailure(result), result.requestId, [
|
|
3903
|
-
`attempt ${attempt} of ${attempts}`
|
|
3904
|
-
])
|
|
3905
|
-
};
|
|
3906
|
-
}
|
|
3907
|
-
lastRequestId = result.requestId;
|
|
3908
|
-
const body = result.value;
|
|
3909
|
-
if (!isJsonObject(body) || !isJsonObject(body["capabilities"])) {
|
|
3910
|
-
return {
|
|
3911
|
-
capabilities: null,
|
|
3912
|
-
step: step(
|
|
3913
|
-
"poll-entitlements",
|
|
3914
|
-
title,
|
|
3915
|
-
"FAIL",
|
|
3916
|
-
`the verdict answered ${result.status} without a capabilities map`,
|
|
3917
|
-
result.requestId,
|
|
3918
|
-
["the verdict is fail-closed: an empty map is normal, a missing key is not"]
|
|
3919
|
-
)
|
|
3920
|
-
};
|
|
3921
|
-
}
|
|
3922
|
-
observed = Object.keys(body["capabilities"]).sort();
|
|
3923
|
-
const revision = typeof body["revision"] === "number" ? body["revision"] : -1;
|
|
3924
|
-
if (observed.length > 0) {
|
|
3925
|
-
return {
|
|
3926
|
-
capabilities: observed,
|
|
3927
|
-
step: step(
|
|
3928
|
-
"poll-entitlements",
|
|
3929
|
-
title,
|
|
3930
|
-
"PASS",
|
|
3931
|
-
`read a verdict at revision ${revision} on attempt ${attempt} of ${attempts}`,
|
|
3932
|
-
result.requestId,
|
|
3933
|
-
[`granted: ${observed.join(", ")}`]
|
|
3934
|
-
)
|
|
3935
|
-
};
|
|
3936
|
-
}
|
|
3937
|
-
if (attempt < attempts) await options.clock.sleep(options.pollDelayMs);
|
|
3938
|
-
}
|
|
3939
|
-
return {
|
|
3940
|
-
capabilities: observed,
|
|
3941
|
-
step: step(
|
|
3942
|
-
"poll-entitlements",
|
|
3943
|
-
title,
|
|
3944
|
-
"PASS",
|
|
3945
|
-
`read a verdict on all ${attempts} attempts; it granted nothing`,
|
|
3946
|
-
lastRequestId,
|
|
3947
|
-
["the read itself worked; whether an empty verdict is acceptable is the next step"]
|
|
3948
|
-
)
|
|
3949
|
-
};
|
|
3950
|
-
}
|
|
3951
|
-
function assertVerdict(capabilities, poll) {
|
|
3952
|
-
const title = "assert the verdict is not empty";
|
|
3953
|
-
if (capabilities === null) {
|
|
3954
|
-
return step("assert-verdict", title, "SKIP", "no verdict was read, so there is nothing to assert on");
|
|
3955
|
-
}
|
|
3956
|
-
if (capabilities.length === 0) {
|
|
3957
|
-
return step(
|
|
3958
|
-
"assert-verdict",
|
|
3959
|
-
title,
|
|
3960
|
-
"FAIL",
|
|
3961
|
-
"the verdict is empty: this caller is granted no capabilities at all",
|
|
3962
|
-
poll.requestId,
|
|
3963
|
-
[
|
|
3964
|
-
"an empty verdict has several distinct causes and carries no reason of its own",
|
|
3965
|
-
"run `everfur diagnose` for the machine code and next action for each capability"
|
|
3966
|
-
]
|
|
3967
|
-
);
|
|
3968
|
-
}
|
|
3969
|
-
return step(
|
|
3970
|
-
"assert-verdict",
|
|
3971
|
-
title,
|
|
3972
|
-
"PASS",
|
|
3973
|
-
`the verdict grants ${capabilities.length} capabilit${capabilities.length === 1 ? "y" : "ies"}`,
|
|
3974
|
-
poll.requestId,
|
|
3975
|
-
[capabilities.join(", ")]
|
|
3976
|
-
);
|
|
3977
|
-
}
|
|
3978
|
-
async function runVerification(options) {
|
|
3979
|
-
const mint = await mintSession(options);
|
|
3980
|
-
const poll = await pollEntitlements(options, mint.bearer);
|
|
3981
|
-
const assertion = assertVerdict(poll.capabilities, poll.step);
|
|
3982
|
-
const steps = Object.freeze([mint.step, poll.step, assertion]);
|
|
3983
|
-
const failed = steps.some((entry) => entry.result === "FAIL");
|
|
3984
|
-
const skipped = steps.some((entry) => entry.result === "SKIP");
|
|
3985
|
-
return Object.freeze({
|
|
3986
|
-
steps,
|
|
3987
|
-
status: failed ? "failed" : skipped ? "incomplete" : "passed"
|
|
3988
|
-
});
|
|
3989
|
-
}
|
|
3990
|
-
|
|
3991
|
-
// src/version.ts
|
|
3992
|
-
var CLI_VERSION = "0.1.0";
|
|
3993
|
-
|
|
3994
|
-
export { ADMIN_TOKEN, API_PREFIX, BACKEND_LANGUAGE_QUESTION, BASE_URL_ENV_VAR, CAPABILITIES_TREE, CAPABILITY_QUESTION, CLI_VERSION, CREDENTIAL_SPECS, DEFAULT_TIMEOUT_MS, EMPTY_REGISTRY, ERRORS_DOCUMENT_KEY, IDEMPOTENT_METHODS, IDENTITY_QUESTION, IDENTITY_TREE, MAX_TIMEOUT_MS, MIN_TIMEOUT_MS, MISSING_FACT_NAMES, PARTNER_KEY, PARTNER_SECRET_KEY, PLAN_DOCUMENT_KIND, PLAN_SCHEMA_VERSION, REDACTED, SCHEMAS, SESSION_MINT_TREE, SESSION_TOKEN, TransportError, apiUrl, buildDecisionSpace, clampTimeout, createApiClient, createRecordingClock, describeFailure, detectProject, digestOfProjection, digestsEqual, entryFromRow, ephemeralCredential, errorCodeOf, failureDetails, fetchTransport, isCorrelatableRequestId, isIdempotent, isJsonObject, loadBundledDecisionSpace, loadDecisionSpace, loadErrorRegistry, missingFactsFor, nodeFileSystem, planIntegration, resolveContractPath, resolveCredential, resolveEndpoint, runVerification, systemClock, validateContractFile, validateContractPublicEnvelope, validateDiagnosisDocument, validateErrorExplanationDocument, validatePlanDocument, validateSandboxCreateDocument, validateVerificationDocument, verifyDigest, walkSingle, walkTree, withPrefix };
|