@looply/sdk 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js ADDED
@@ -0,0 +1,520 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LooplyClient = void 0;
4
+ const errors_1 = require("./errors");
5
+ /**
6
+ * A ceiling on every request.
7
+ *
8
+ * A call with no deadline is a spinner that never stops. Fifteen seconds is the
9
+ * app's existing choice and is kept so the two behave identically.
10
+ */
11
+ const DEFAULT_TIMEOUT_MS = 15_000;
12
+ class LooplyClient {
13
+ opts;
14
+ http;
15
+ constructor(opts) {
16
+ this.opts = opts;
17
+ const f = opts.fetch ?? globalThis.fetch;
18
+ if (typeof f !== 'function') {
19
+ throw new Error('no fetch is available; pass one in LooplyClientOptions.fetch');
20
+ }
21
+ this.http = f;
22
+ }
23
+ /* ──────────────────────────────────────────────────────────── projects ── */
24
+ /**
25
+ * ⚠️ THIS ROUTE ANSWERS A BARE ARRAY, NOT `{ projects }`.
26
+ *
27
+ * 🔴 THE FIRST DRAFT UNWRAPPED AN ENVELOPE THAT DOES NOT EXIST, and returned
28
+ * `undefined` typed as `ProjectView[]` — the cast asserted the envelope into
29
+ * being and TypeScript had nothing to object to. It was caught by
30
+ * `sdk-flagship.e2e.spec.ts`, which drives this client against a REAL server;
31
+ * a suite that mocked `fetch` would have returned whatever shape this method
32
+ * asked for and agreed with itself all the way to production.
33
+ */
34
+ async projects() {
35
+ return await this.request('GET', '/v1/projects');
36
+ }
37
+ /* ─────────────────────────────────────────────────────────────── goals ── */
38
+ /**
39
+ * Create a goal, at most once per key.
40
+ *
41
+ * 🔴 `clientRef` IS THE IDEMPOTENCY KEY AND IT IS REQUIRED. There is no
42
+ * DELETE for a goal, so a duplicate from a retried POST is permanent. Sending
43
+ * the same key with the SAME text returns the same goal; sending it with
44
+ * DIFFERENT text is a conflict, because the key was a promise about which
45
+ * request this is.
46
+ */
47
+ async createGoal(projectId, input) {
48
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/goals`, input);
49
+ return r.goal;
50
+ }
51
+ async goal(projectId, goalId) {
52
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/goals/${enc(goalId)}`);
53
+ return r.goal;
54
+ }
55
+ async goals(projectId, state) {
56
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/goals`, undefined, { state });
57
+ return r.goals;
58
+ }
59
+ /* ───────────────────────────────────────────────────────────── sources ── */
60
+ /**
61
+ * Hand a document over, at most once per key AND at most once per content.
62
+ *
63
+ * A source is stored verbatim and content-addressed: the same bytes under a
64
+ * fresh `clientRef` answer with the source that already exists, and the same
65
+ * `clientRef` with different bytes is a conflict. What comes back is the
66
+ * stored record with its anchored statements — the extractor's answer, so a
67
+ * caller can see what Looply read before anything is built on it.
68
+ *
69
+ * ⚠️ `kind` is `markdown`, `text` or `pdf`. The text kinds send `content`;
70
+ * a `pdf` sends the `pages` of its text layer (as read on the machine that
71
+ * holds the file) and may carry the file's own sha256 as `originSha256`.
72
+ */
73
+ async createSource(projectId, input) {
74
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/sources`, input);
75
+ return detailView(r);
76
+ }
77
+ /**
78
+ * Classify a source with the current classifier version, once.
79
+ *
80
+ * Idempotent per version: the rows that exist are answered as they are.
81
+ * What comes back says what each statement IS; nothing here says what the
82
+ * repository has done about it.
83
+ */
84
+ async classifySource(projectId, sourceId) {
85
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/sources/${enc(sourceId)}/classify`);
86
+ return { classifications: r.classifications, classifierVersion: r.classifierVersion, created: r.created };
87
+ }
88
+ /**
89
+ * Record where each requirement of a source stands against the repository,
90
+ * as measured at one committed tree — once per (tree, reconciler version).
91
+ *
92
+ * The measurement is made where the project index lives (the machine that
93
+ * holds the repository; `looply source reconcile` is the usual caller) with
94
+ * `@looply/core`'s `reconcileStatements`; the server stores it verbatim and
95
+ * recomputes nothing. A second hand-over of the same tree answers the rows
96
+ * on record (`created: false`).
97
+ *
98
+ * 🔴 `implemented-and-evidenced` and `partial` are REFUSED here (400): a
99
+ * rule cannot put them on record. They are a person's attestation with
100
+ * evidence, which is a different act.
101
+ */
102
+ async reconcileSource(projectId, sourceId, input) {
103
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/sources/${enc(sourceId)}/reconcile`, input);
104
+ return { dispositions: r.dispositions, treeSha: r.treeSha, reconcilerVersion: r.reconcilerVersion, created: r.created };
105
+ }
106
+ /**
107
+ * Say, as a person, that a requirement is implemented-and-evidenced or
108
+ * partial — with at least one evidence anchor, at the committed tree the
109
+ * evidence was read at.
110
+ *
111
+ * 🔴 THIS NEEDS A DEVICE TOKEN. A machine token is refused (403): the machine
112
+ * measures (`reconcileSource`), a person vouches. A measurement's values are
113
+ * refused here (400). The same person saying the same thing at the same tree
114
+ * is one act: a repeat answers the record with `created: false`.
115
+ */
116
+ async attestSource(projectId, sourceId, input) {
117
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/sources/${enc(sourceId)}/attest`, input);
118
+ return { attestation: r.attestation, created: r.created };
119
+ }
120
+ /**
121
+ * What is covered and by whom: every requirement and constraint of a source
122
+ * with the goals that serve it, where it stands, what a person attested, and
123
+ * one word each. A read; nothing here is written by anyone.
124
+ */
125
+ async coverage(projectId, sourceId) {
126
+ return this.request('GET', `/v1/projects/${enc(projectId)}/sources/${enc(sourceId)}/coverage`);
127
+ }
128
+ /**
129
+ * Every session-control grant this project has issued, live and historical.
130
+ *
131
+ * 🔴 THE MACHINE READS THESE AND WRITES NONE OF THEM. `OWNER_CHARTER.md` §5
132
+ * F-14 is explicit that only the server may issue one — a machine that could
133
+ * mint its own would be granting itself authority over a session, which is
134
+ * the whole thing the grant exists to prevent. So there is a reader here and
135
+ * deliberately no `issueSessionGrant`: issuing is a person's act, from a
136
+ * person's device, against the route that stamps their principal onto it.
137
+ */
138
+ async sessionGrants(projectId) {
139
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/session-grants`);
140
+ return r.grants;
141
+ }
142
+ /**
143
+ * Record that a session acknowledged a grant, through the channel it came on.
144
+ *
145
+ * ⚠️ THE CHANNEL IS CLAIMED, NOT PROVED, AND THAT IS WHY IT IS RECORDED
146
+ * RATHER THAN TRUSTED. A session's adapter and a person's terminal can hold
147
+ * the same device token — the token says which DEVICE, never which program.
148
+ * So this writes down what arrived and `controlVerdict` in core refuses
149
+ * anything that is not `adapter`, which turns a person's attempt into a fact
150
+ * on the record instead of an inference nobody can check.
151
+ */
152
+ async acknowledgeSessionGrant(projectId, grantId, ack) {
153
+ const r = await this.request('POST', `/v1/projects/${enc(projectId)}/session-grants/${enc(grantId)}/acknowledge`, ack);
154
+ return r.grant;
155
+ }
156
+ /** A project's sources, newest first, without their content. */
157
+ async sources(projectId) {
158
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/sources`);
159
+ return r.sources;
160
+ }
161
+ /** One source, verbatim, with its anchored statements, their latest classifications and the most recent reconciliation. */
162
+ async source(projectId, sourceId) {
163
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/sources/${enc(sourceId)}`);
164
+ return detailView(r);
165
+ }
166
+ /**
167
+ * Ask Looply to work on a goal, once.
168
+ *
169
+ * 🔴 ONE EXPLICIT START ≤ ONE RUN, AND A LOST RESPONSE MUST NOT SPEND TWO.
170
+ * `clientRef` is what makes a retry safe: the same key names the same act of
171
+ * permission, so a repeat returns the command the first call created rather
172
+ * than authorising a second Start.
173
+ *
174
+ * ⚠️ IT IS REQUIRED HERE THOUGH THE SERVER TREATS IT AS OPTIONAL. The server
175
+ * has to accept keyless commands because the phone and the CLI have always
176
+ * sent none; an SDK written today has no such history and no reason to offer
177
+ * the unsafe shape. A caller who genuinely wants at-least-once can pass a
178
+ * fresh key each time — deliberately, rather than by omission.
179
+ *
180
+ * 🔴 THIS DEFECT WAS FOUND BY WRITING THIS METHOD. `POST /v1/commands` had no
181
+ * key at all, and the daemon derives a Start's request id from the COMMAND
182
+ * ID, so a retried enqueue was a second Start spending a second ordinal of a
183
+ * person's autonomy budget. Phase 15 added the key to the server.
184
+ */
185
+ async startGoal(projectId, goalId, input) {
186
+ /**
187
+ * ⚠️ THERE IS NO `/goals/:id/start` ROUTE, AND INVENTING ONE HERE WOULD BE
188
+ * THE DEFECT §77 NAMES: an SDK method with no real route behind it. Work is
189
+ * started by issuing a COMMAND, which is how the daemon learns of it — the
190
+ * app has done this since Phase 4 and the server's verb set is fixed.
191
+ *
192
+ * 🔴 NO `goalVersion` IS SENT, DELIBERATELY. The server stamps the version
193
+ * it just read and REFUSES a client-supplied one, so that a phone which has
194
+ * been asleep cannot authorise pursuit of wording nobody is looking at. The
195
+ * absence is the safety property, not an omission.
196
+ *
197
+ * Naming no items means "start this goal on the work it already owns".
198
+ */
199
+ const r = await this.request('POST', '/v1/commands', {
200
+ projectId,
201
+ kind: 'start-run',
202
+ payload: { goalId },
203
+ clientRef: input.clientRef,
204
+ ...(input.deliverBy === undefined ? {} : { deliverBy: input.deliverBy }),
205
+ });
206
+ return { commandId: r.id };
207
+ }
208
+ /**
209
+ * Authorise BOUNDED AUTONOMOUS PURSUIT of a goal: the machine plans, runs and
210
+ * re-plans on its own, up to the server's budget (Starts and lifetime), until
211
+ * the goal is met, a person is needed, or the budget runs out.
212
+ *
213
+ * 🔴 THIS, NOT `startGoal`, IS WHAT AN UNATTENDED CALLER WANTS. `startGoal` is
214
+ * ONE run: when it ends nothing says whether the goal was met. A pursuit ends
215
+ * with a verdict — `satisfied`, `human_required`, `budget_exhausted` — read by
216
+ * `autonomy()` and `goalEvidence()`, and on `satisfied` the machine's own
217
+ * delivery comes with it.
218
+ *
219
+ * The SERVER stamps the version, the budget and the expiry; the client names
220
+ * the goal and nothing else, so a caller cannot pin a budget it was not given.
221
+ * Idempotent on `clientRef`, which matters doubly here: authorising SPENDS the
222
+ * grant, so a lost response retried without the key would spend it twice.
223
+ */
224
+ async authorizeGoalAutonomy(projectId, goalId, input) {
225
+ const r = await this.request('POST', '/v1/commands', {
226
+ projectId,
227
+ kind: 'authorize-goal-autonomy',
228
+ payload: { goalId },
229
+ clientRef: input.clientRef,
230
+ ...(input.deliverBy === undefined ? {} : { deliverBy: input.deliverBy }),
231
+ });
232
+ return { commandId: r.id };
233
+ }
234
+ /**
235
+ * Is the machine that holds this project awake? A Start is delivered by the
236
+ * machine polling, so this is the first half of "will my Start run".
237
+ */
238
+ async presence(projectId) {
239
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/presence`);
240
+ }
241
+ /**
242
+ * What became of a command.
243
+ *
244
+ * `startGoal` returns an id and then goes quiet; this is the durable answer —
245
+ * started, terminally refused, and why.
246
+ *
247
+ * ⚠️ COMMAND TRUTH ONLY. `done` on a start-run means the Start reached its
248
+ * success boundary, NOT that the run finished or the goal was met. A caller
249
+ * wanting the whole picture composes this with `status()` and `events()`.
250
+ */
251
+ async commandStatus(projectId, commandId) {
252
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/commands/${enc(commandId)}`);
253
+ return r.command;
254
+ }
255
+ /** Amend or abandon a goal. `version` is the OCC token the read returned. */
256
+ async patchGoal(projectId, goalId, input) {
257
+ const r = await this.request('PATCH', `/v1/projects/${enc(projectId)}/goals/${enc(goalId)}`, input);
258
+ return r.goal;
259
+ }
260
+ /* ──────────────────────────────────────────────────────────────── work ── */
261
+ async items(projectId) {
262
+ const r = await this.request('GET', `/v1/projects/${enc(projectId)}/queue`);
263
+ return r.items;
264
+ }
265
+ /**
266
+ * The current run, as a projection.
267
+ *
268
+ * ⚠️ THERE IS NO `/runs` COLLECTION, so this SDK does not pretend to one. The
269
+ * server keeps a single latest-run projection per project — which is what the
270
+ * product actually shows — and run HISTORY lives in the event ledger, reached
271
+ * through `events()`.
272
+ */
273
+ async status(projectId) {
274
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/status`);
275
+ }
276
+ /* ─────────────────────────────────────────────────────────── keeper ── */
277
+ /**
278
+ * Ask the keeper about a project (DESIGN §9 read path).
279
+ *
280
+ * A POST that READS. The question travels in the body rather than a query
281
+ * string, because a sentence about somebody's codebase does not belong in an
282
+ * access log — and because the answer quotes item titles, commit shas and the
283
+ * implementer's own round notes, it needs the same grant `status()` does.
284
+ *
285
+ * ⚠️ NOTHING HERE ACTS. The keeper’s write path is deliberately not a second
286
+ * set of methods: an intent goes through the routes a person already uses —
287
+ * `command()`, `patchItem()`, answering a question — with `actor: keeper` on
288
+ * the record, so there is no privileged path for it to take.
289
+ */
290
+ async ask(projectId, question) {
291
+ return await this.request('POST', `/v1/projects/${enc(projectId)}/keeper/ask`, { question });
292
+ }
293
+ /* ─────────────────────────────────────────────────────────── approvals ── */
294
+ /**
295
+ * ⚠️ THE APPROVAL ROUTES ANSWER BARE VALUES, NOT ENVELOPES.
296
+ *
297
+ * The goal and queue routes DO wrap (`{ goals }`, `{ items }`) and these do
298
+ * not. There is no rule to infer here — it is simply how each controller was
299
+ * written — which is exactly why this client is checked against a running
300
+ * server rather than against a design document.
301
+ */
302
+ async approvals(projectId) {
303
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/approvals`);
304
+ }
305
+ async approval(projectId, approvalId) {
306
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/approvals/${enc(approvalId)}`);
307
+ }
308
+ /**
309
+ * Advise on a pending approval: approve or deny, and why.
310
+ *
311
+ * 🔴 THIS IS WHAT AN INTEGRATION CAN DO WITH AN APPROVAL, AND ALL IT CAN DO.
312
+ * Deciding is a person's authority (`decideApproval` needs an owner's device
313
+ * token); an integration that has reviewed the action in its own process says
314
+ * what it concluded here, and the owner is notified and decides with the
315
+ * advice in front of them. Pending only (a 409 otherwise); latest wins, so a
316
+ * retry is harmless.
317
+ */
318
+ async recommendApproval(projectId, approvalId, input) {
319
+ return await this.request('POST', `/v1/projects/${enc(projectId)}/approvals/${enc(approvalId)}/recommendation`, input);
320
+ }
321
+ /**
322
+ * Approve or deny one exact action.
323
+ *
324
+ * 🔴 A PERSON DOES THIS. A machine credential is refused by the server, and
325
+ * that refusal is the security content of the route rather than an
326
+ * inconvenience — the hazard is not the 403, it is somebody meeting it and
327
+ * widening the guard so a machine can authorise its own work.
328
+ *
329
+ * `version` is the approval's OCC token: deciding against a stale one is a
330
+ * conflict, not a silent overwrite, because the thing being approved may have
331
+ * expired or been revoked since it was read.
332
+ */
333
+ async decideApproval(projectId, approvalId, input) {
334
+ return await this.request('POST', `/v1/projects/${enc(projectId)}/approvals/${enc(approvalId)}/decision`, input);
335
+ }
336
+ /* ───────────────────────────────────────────────── governed execution ── */
337
+ async execution(projectId, executionId) {
338
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/executions/${enc(executionId)}`);
339
+ }
340
+ /**
341
+ * Tell Looply what actually happened, when it could not establish that itself.
342
+ *
343
+ * 🔴 THIS CLOSES A GAP PHASE 8 LEFT ON PURPOSE. The resolution route has been
344
+ * owner-guarded and mutation-covered since Phase 8 and had NO CLIENT — and its
345
+ * client could not be the CLI, because `server-client.ts` authenticates with a
346
+ * machine token and the route refuses a machine BY DESIGN. A machine settling
347
+ * its own ambiguity is the one thing the route exists to prevent. It belongs
348
+ * to a person, so it belongs here.
349
+ *
350
+ * ⚠️ `note` IS PRIVATE HUMAN TESTIMONY. It is why a person believes what they
351
+ * are asserting, it is kept out of every machine- and model-facing view, and a
352
+ * client must never render it into anything a model reads.
353
+ */
354
+ async resolveExecution(projectId, executionId, input) {
355
+ return await this.request('POST', `/v1/projects/${enc(projectId)}/executions/${enc(executionId)}/resolution`, input);
356
+ }
357
+ /* ────────────────────────────────────────────────────────────── safety ── */
358
+ /**
359
+ * Whether autonomous pursuit of a goal is still running, and why it stopped.
360
+ *
361
+ * ⚠️ THIS IS THE AUTONOMY VIEW, NOT A SAFETY ENDPOINT. There is no
362
+ * `/safety` route: a loop-safety hold is enforced ON THE MACHINE, in
363
+ * `loop-safety-store.ts`, and never travels to the server. What the server
364
+ * knows is that pursuit ENDED and the reason it was given — which is what a
365
+ * person needs to see, and is version-scoped because a hold on version 4 must
366
+ * not block version 5.
367
+ */
368
+ async autonomy(projectId, goalId) {
369
+ // Returned whole. The grants and the goal's current version arrive in one
370
+ // response so a caller cannot act on two answers from two moments.
371
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/goals/${enc(goalId)}/autonomy`);
372
+ }
373
+ /* ──────────────────────────────────────────────────────────── evidence ── */
374
+ /**
375
+ * What the server saw of a goal being delivered, per version.
376
+ *
377
+ * This used to say there was no `/evidence` route and that one would be a
378
+ * server change, not an SDK one. It is now that server change:
379
+ * `GET …/goals/:goalId/evidence` stitches, once, the runs that pursued each
380
+ * version, every round's commit with the gate's and the prover's verdicts,
381
+ * where each pursuit came to rest, and what was pushed under an approval.
382
+ *
383
+ * ⚠️ NOT THE MACHINE'S PROOF. Whether a goal was met is decided on the
384
+ * machine; this is what reached the server. Render `pursuits[].state ===
385
+ * 'satisfied'` as the machine's claim and `landed` as the commits behind it —
386
+ * never as "proven". Whitelisted: no gate output, no prover prose, no remote.
387
+ */
388
+ async goalEvidence(projectId, goalId) {
389
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/goals/${enc(goalId)}/evidence`);
390
+ }
391
+ /* ────────────────────────────────────────────────────────────── events ── */
392
+ /**
393
+ * One page of events, resuming from a cursor.
394
+ *
395
+ * 🔴 REST REPLAY IS THE CORRECTNESS PATH; A SOCKET IS ONLY AN ACCELERATOR. A
396
+ * client that has been away — asleep, offline, restarted — reconstructs by
397
+ * paging from the last cursor it stored. A live stream that missed an event
398
+ * has no way to notice, so it may never be the only way state arrives.
399
+ *
400
+ * ⚠️ DUPLICATES ARE SAFE AND EXPECTED. `id` is stable, so a client that has
401
+ * seen one can drop it; a client that has NOT seen one must not be missing it.
402
+ * Delivering twice is the cheap failure, and not delivering is the expensive
403
+ * one.
404
+ */
405
+ async events(projectId, opts = {}) {
406
+ return await this.request('GET', `/v1/projects/${enc(projectId)}/platform-events`, undefined, { after: opts.after, limit: opts.limit });
407
+ }
408
+ /* ───────────────────────────────────────────────────────────── plumbing ── */
409
+ /**
410
+ * 🔴 THE QUERY IS A SEPARATE ARGUMENT, NEVER INTERPOLATED INTO `path`.
411
+ *
412
+ * `path` is therefore always a literal route with parameter slots and nothing
413
+ * else, which is what lets `boundary.spec.ts` compare it to the server's route
414
+ * table exactly rather than by heuristic. An earlier version appended
415
+ * `${q}` inside the template and the guard could not tell a query suffix from
416
+ * a path segment — it read `/goals?state=x` as a route called `/goals:param`.
417
+ * Fixing the guard's parser instead would have left the ambiguity in place for
418
+ * the next method to trip over.
419
+ *
420
+ * `undefined` values are dropped rather than sent as the string "undefined".
421
+ */
422
+ async request(method, path, body, query) {
423
+ const controller = new AbortController();
424
+ const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
425
+ const token = this.opts.getToken();
426
+ const params = new URLSearchParams();
427
+ for (const [k, v] of Object.entries(query ?? {})) {
428
+ if (v !== undefined)
429
+ params.set(k, String(v));
430
+ }
431
+ const suffix = params.toString() === '' ? '' : `?${params.toString()}`;
432
+ let res;
433
+ try {
434
+ res = await this.http(`${this.opts.baseUrl}${path}${suffix}`, {
435
+ method,
436
+ signal: controller.signal,
437
+ headers: {
438
+ 'Content-Type': 'application/json',
439
+ ...(token === null ? {} : { Authorization: `Bearer ${token}` }),
440
+ },
441
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
442
+ });
443
+ }
444
+ catch (e) {
445
+ clearTimeout(timer);
446
+ const aborted = e?.name === 'AbortError';
447
+ // Status 0: there was no HTTP answer at all. Distinguished from a 5xx
448
+ // because "no server" and "server said no" are different facts.
449
+ throw new errors_1.LooplyError({
450
+ status: 0,
451
+ message: aborted ? 'the request timed out' : 'could not reach looply',
452
+ serverCode: aborted ? 'timeout' : 'unavailable',
453
+ });
454
+ }
455
+ clearTimeout(timer);
456
+ const text = await res.text();
457
+ const parsed = text === '' ? null : safeJson(text);
458
+ if (!res.ok) {
459
+ if (res.status === 401)
460
+ this.opts.onUnauthorized?.();
461
+ throw new errors_1.LooplyError({
462
+ status: res.status,
463
+ message: messageOf(parsed) ?? `request failed (${res.status})`,
464
+ serverCode: codeOf(parsed),
465
+ currentVersion: versionOf(parsed),
466
+ body: parsed,
467
+ });
468
+ }
469
+ return parsed;
470
+ }
471
+ }
472
+ exports.LooplyClient = LooplyClient;
473
+ /** Path segments are user data. A goal id with a slash must not become a route. */
474
+ function enc(s) {
475
+ return encodeURIComponent(s);
476
+ }
477
+ function safeJson(text) {
478
+ try {
479
+ return JSON.parse(text);
480
+ }
481
+ catch {
482
+ // A body that is not JSON is a proxy or a gateway talking, not Looply.
483
+ return { message: text.slice(0, 300) };
484
+ }
485
+ }
486
+ function field(body, name) {
487
+ return typeof body === 'object' && body !== null && name in body
488
+ ? body[name]
489
+ : undefined;
490
+ }
491
+ function messageOf(body) {
492
+ const m = field(body, 'message');
493
+ if (typeof m === 'string')
494
+ return m;
495
+ // Nest's validation pipe answers with an ARRAY of messages.
496
+ if (Array.isArray(m) && typeof m[0] === 'string')
497
+ return m.join('; ');
498
+ return undefined;
499
+ }
500
+ function codeOf(body) {
501
+ const c = field(body, 'code');
502
+ return typeof c === 'string' ? c : undefined;
503
+ }
504
+ function versionOf(body) {
505
+ const v = field(body, 'currentVersion') ?? field(body, 'version');
506
+ return typeof v === 'number' ? v : undefined;
507
+ }
508
+ /** The detail as the wire carries it (a `source` record beside its derived rows), flattened into one view. */
509
+ function detailView(r) {
510
+ return {
511
+ ...r.source,
512
+ statements: r.statements,
513
+ classifications: r.classifications,
514
+ classifierVersion: r.classifierVersion,
515
+ dispositions: r.dispositions,
516
+ reconciledAt: r.reconciledAt,
517
+ attestations: r.attestations,
518
+ };
519
+ }
520
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * WHAT WENT WRONG, IN A FORM A PROGRAM CAN BRANCH ON.
3
+ *
4
+ * ─────────────────────────────────────────────────────────────────────────────
5
+ * 🔴 NO CLIENT MAY EVER PARSE AN ENGLISH MESSAGE.
6
+ *
7
+ * The server's wire shape is `{ statusCode, message }`, and the messages are
8
+ * good ones — controllers put real work into them, and a person reading a
9
+ * refusal should get a sentence rather than a symbol. But a message is written
10
+ * for a person and is changed for a person, so a client that branches on its
11
+ * text breaks the next time somebody improves the wording. That is the whole
12
+ * reason for this file.
13
+ *
14
+ * ⚠️ THE CODES ARE DERIVED FROM STATUS, NOT INVENTED ALONGSIDE IT. There are
15
+ * roughly 250 throw sites in the server and none of them carries a code today.
16
+ * Adding one to each would be a large, mechanical, error-prone edit whose only
17
+ * consumer is this file — so instead the mapping lives here, where it can be
18
+ * read in one screen, and a `code` the server DOES supply is preferred over the
19
+ * derived one. That is a narrower promise than "every refusal has a bespoke
20
+ * code", and it is the one that is true.
21
+ */
22
+ export type LooplyErrorCode =
23
+ /** The credential is missing, expired or revoked. Signing in again is the fix. */
24
+ 'unauthorized'
25
+ /** Authenticated, and not allowed to do this. Retrying changes nothing. */
26
+ | 'forbidden'
27
+ /**
28
+ * There is no such resource — OR there is one and you may not know that.
29
+ *
30
+ * 🔴 THE AMBIGUITY IS DELIBERATE AND MUST BE PRESERVED. Several routes answer
31
+ * 404 for another project's resource on purpose, so that probing cannot tell
32
+ * "does not exist" from "not yours". A client that reported those differently
33
+ * would rebuild the oracle the server refuses to be.
34
+ */
35
+ | 'not_found'
36
+ /**
37
+ * It existed, it is finished, and no retry brings it back.
38
+ *
39
+ * 🔴 DELIBERATELY NOT FOLDED INTO `not_found`, whose ambiguity above is load-
40
+ * bearing: 404 has to mean "no such thing, OR yours to not know about" so that
41
+ * probing another project's resources learns nothing. A 410 makes no such
42
+ * hedge — the server is saying it knows exactly what you asked for and it is
43
+ * over. An activation code that expired or was withdrawn is the first one;
44
+ * `apps/server/src/membership/membership.controller.ts` is where it is thrown.
45
+ *
46
+ * ⚠️ AND NOT `unavailable`, WHICH IS RETRYABLE. That was the live defect: the
47
+ * status census below caught 410 arriving unmapped, and unmapped means
48
+ * `unavailable`, which `isRetryable` calls true — so a caller would have been
49
+ * told to try an expired code again, forever.
50
+ */
51
+ | 'gone'
52
+ /**
53
+ * The request lost a race, or repeated one it had already won differently.
54
+ *
55
+ * Carries the current version where the server supplies one, so an optimistic
56
+ * caller can re-read and retry rather than guess.
57
+ */
58
+ | 'conflict'
59
+ /** The request itself was wrong. Retrying it unchanged will fail identically. */
60
+ | 'invalid_request'
61
+ /** Looply could not be reached, or answered with a defect. Retryable. */
62
+ | 'unavailable'
63
+ /** No answer within the client's own deadline. Retryable, and may have landed. */
64
+ | 'timeout';
65
+ /**
66
+ * Whether trying the SAME request again could plausibly succeed.
67
+ *
68
+ * ⚠️ `timeout` IS RETRYABLE AND ALSO THE MOST DANGEROUS TO RETRY, which is why
69
+ * every mutating call in this SDK takes an idempotency key. A timed-out request
70
+ * may have been executed; retrying without a key is how one Start becomes two.
71
+ */
72
+ export declare function isRetryable(code: LooplyErrorCode): boolean;
73
+ /**
74
+ * The code for an HTTP status.
75
+ *
76
+ * Anything unmapped is `unavailable`: an unrecognised 5xx is Looply failing, and
77
+ * an unrecognised 4xx is a refusal this client does not model — which the caller
78
+ * should treat as "not now" rather than as a specific promise.
79
+ */
80
+ export declare function codeForStatus(status: number): LooplyErrorCode;
81
+ export interface LooplyErrorInit {
82
+ readonly status: number;
83
+ readonly message: string;
84
+ /** A code the SERVER supplied, which always wins over the derived one. */
85
+ readonly serverCode?: string;
86
+ /** The current version on a conflict, where the server names one. */
87
+ readonly currentVersion?: number;
88
+ readonly body?: unknown;
89
+ }
90
+ /**
91
+ * The one error type this SDK throws.
92
+ *
93
+ * 🔴 EVERY FAILURE PATH PRODUCES ONE OF THESE, including a network failure and a
94
+ * timeout — which are not HTTP statuses at all. A caller that has to distinguish
95
+ * "the server said no" from "there was no server" by catching two different
96
+ * shapes will eventually catch only one of them.
97
+ */
98
+ export declare class LooplyError extends Error {
99
+ readonly status: number;
100
+ readonly code: LooplyErrorCode;
101
+ readonly currentVersion: number | undefined;
102
+ readonly body: unknown;
103
+ constructor(init: LooplyErrorInit);
104
+ /** Whether trying this exact request again could plausibly succeed. */
105
+ get retryable(): boolean;
106
+ }
107
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,eAAe;AACzB,kFAAkF;AAChF,cAAc;AAChB,2EAA2E;GACzE,WAAW;AACb;;;;;;;GAOG;GACD,WAAW;AACb;;;;;;;;;;;;;;GAcG;GACD,MAAM;AACR;;;;;GAKG;GACD,UAAU;AACZ,iFAAiF;GAC/E,iBAAiB;AACnB,yEAAyE;GACvE,aAAa;AACf,kFAAkF;GAChF,SAAS,CAAC;AAEd;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAE1D;AAYD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAwB7D;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBAEX,IAAI,EAAE,eAAe;IASjC,uEAAuE;IACvE,IAAI,SAAS,IAAI,OAAO,CAEvB;CACF"}