@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/LICENSE +21 -0
- package/README.md +101 -3
- package/dist/client.d.ts +452 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +520 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +107 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +119 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/views.d.ts +723 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +30 -0
- package/dist/views.js.map +1 -0
- package/package.json +33 -6
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":";;;AAAA,qCAAuC;AAiFvC;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,MAAa,YAAY;IAGM;IAFZ,IAAI,CAA0B;IAE/C,YAA6B,IAAyB;QAAzB,SAAI,GAAJ,IAAI,CAAqB;QACpD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QACzC,IAAI,OAAO,CAAC,KAAK,UAAU,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;OASG;IACH,KAAK,CAAC,QAAQ;QACZ,OAAO,MAAM,IAAI,CAAC,OAAO,CAAgB,KAAK,EAAE,cAAc,CAAC,CAAC;IAClE,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,CACd,SAAiB,EACjB,KASC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,QAAQ,EACtC,KAAK,CACN,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,SAAiB,EAAE,MAAc;QAC1C,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,MAAM,CAAC,EAAE,CACtD,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,SAAiB,EAAE,KAAyB;QACtD,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,QAAQ,EACtC,SAAS,EACT,EAAE,KAAK,EAAE,CACV,CAAC;QACF,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,YAAY,CAChB,SAAiB,EACjB,KAOC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CASzB,MAAM,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC5D,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,cAAc,CAClB,SAAiB,EACjB,QAAgB;QAEhB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAIzB,MAAM,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC/E,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC,eAAe,EAAE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC5G,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,eAAe,CACnB,SAAiB,EACjB,QAAgB,EAChB,KAIC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAKzB,MAAM,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;QACvF,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC1H,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,YAAY,CAChB,SAAiB,EACjB,QAAgB,EAChB,KAMC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,QAAQ,CAAC,SAAS,EAChE,KAAK,CACN,CAAC;QACF,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,QAAgB;QAChD,OAAO,IAAI,CAAC,OAAO,CAAqB,KAAK,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACrH,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,aAAa,CAAC,SAAiB;QACnC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAChD,CAAC;QACF,OAAO,CAAC,CAAC,MAAM,CAAC;IAClB,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,uBAAuB,CAC3B,SAAiB,EACjB,OAAe,EACf,GAOC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,mBAAmB,GAAG,CAAC,OAAO,CAAC,cAAc,EAC3E,GAAG,CACJ,CAAC;QACF,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IAED,gEAAgE;IAChE,KAAK,CAAC,OAAO,CAAC,SAAiB;QAC7B,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,CACzC,CAAC;QACF,OAAO,CAAC,CAAC,OAAO,CAAC;IACnB,CAAC;IAED,2HAA2H;IAC3H,KAAK,CAAC,MAAM,CAAC,SAAiB,EAAE,QAAgB;QAC9C,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAQzB,KAAK,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrE,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;IACvB,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,SAAS,CACb,SAAiB,EACjB,MAAc,EACd,KAQC;QAED;;;;;;;;;;;;WAYG;QACH,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAiB,MAAM,EAAE,cAAc,EAAE;YACnE,SAAS;YACT,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,EAAE,MAAM,EAAE;YACnB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;SACzE,CAAC,CAAC;QACH,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,qBAAqB,CACzB,SAAiB,EACjB,MAAc,EACd,KAAkE;QAElE,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAiB,MAAM,EAAE,cAAc,EAAE;YACnE,SAAS;YACT,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,EAAE,MAAM,EAAE;YACnB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;SACzE,CAAC,CAAC;QACH,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;IAC7B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,QAAQ,CAAC,SAAiB;QAC9B,OAAO,MAAM,IAAI,CAAC,OAAO,CAAe,KAAK,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IAC5F,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,aAAa,CAAC,SAAiB,EAAE,SAAiB;QACtD,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,aAAa,GAAG,CAAC,SAAS,CAAC,EAAE,CAC5D,CAAC;QACF,OAAO,CAAC,CAAC,OAAO,CAAC;IACnB,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,SAAS,CACb,SAAiB,EACjB,MAAc,EACd,KASC;QAED,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAC1B,OAAO,EACP,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,MAAM,CAAC,EAAE,EACrD,KAAK,CACN,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,8EAA8E;IAE9E,KAAK,CAAC,KAAK,CAAC,SAAiB;QAC3B,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,OAAO,CAAwB,KAAK,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACnG,OAAO,CAAC,CAAC,KAAK,CAAC;IACjB,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,CAAC,SAAiB;QAC5B,OAAO,MAAM,IAAI,CAAC,OAAO,CAAgB,KAAK,EAAE,gBAAgB,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3F,CAAC;IAED,2EAA2E;IAE3E;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,QAAgB;QAC3C,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,aAAa,EAC3C,EAAE,QAAQ,EAAE,CACb,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;OAOG;IACH,KAAK,CAAC,SAAS,CAAC,SAAiB;QAC/B,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,YAAY,CAC3C,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,UAAkB;QAClD,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,UAAU,CAAC,EAAE,CAC9D,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,iBAAiB,CACrB,SAAiB,EACjB,UAAkB,EAClB,KAAwE;QAExE,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,UAAU,CAAC,iBAAiB,EAC5E,KAAK,CACN,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,cAAc,CAClB,SAAiB,EACjB,UAAkB,EAClB,KAsBC;QAED,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,UAAU,CAAC,WAAW,EACtE,KAAK,CACN,CAAC;IACJ,CAAC;IAED,6EAA6E;IAE7E,KAAK,CAAC,SAAS,CAAC,SAAiB,EAAE,WAAmB;QACpD,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,eAAe,GAAG,CAAC,WAAW,CAAC,EAAE,CAChE,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,gBAAgB,CACpB,SAAiB,EACjB,WAAmB,EACnB,KASC;QAED,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,MAAM,EACN,gBAAgB,GAAG,CAAC,SAAS,CAAC,eAAe,GAAG,CAAC,WAAW,CAAC,aAAa,EAC1E,KAAK,CACN,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;OASG;IACH,KAAK,CAAC,QAAQ,CAAC,SAAiB,EAAE,MAAc;QAC9C,0EAA0E;QAC1E,mEAAmE;QACnE,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,MAAM,CAAC,WAAW,CAC/D,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,YAAY,CAAC,SAAiB,EAAE,MAAc;QAClD,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,MAAM,CAAC,WAAW,CAC/D,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,MAAM,CACV,SAAiB,EACjB,OAA6D,EAAE;QAE/D,OAAO,MAAM,IAAI,CAAC,OAAO,CACvB,KAAK,EACL,gBAAgB,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAChD,SAAS,EACT,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CACzC,CAAC;IACJ,CAAC;IAED,+EAA+E;IAE/E;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,OAAO,CACnB,MAAc,EACd,IAAY,EACZ,IAAc,EACd,KAA6D;QAE7D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC,CAAC;QAC9F,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAEnC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAEvE,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,MAAM,EAAE,EAAE;gBAC5D,MAAM;gBACN,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,CAAC;iBAChE;gBACD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;aAC9D,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,OAAO,GAAI,CAAW,EAAE,IAAI,KAAK,YAAY,CAAC;YACpD,sEAAsE;YACtE,gEAAgE;YAChE,MAAM,IAAI,oBAAW,CAAC;gBACpB,MAAM,EAAE,CAAC;gBACT,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,wBAAwB;gBACrE,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa;aAChD,CAAC,CAAC;QACL,CAAC;QACD,YAAY,CAAC,KAAK,CAAC,CAAC;QAEpB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAY,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAE5D,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YACrD,MAAM,IAAI,oBAAW,CAAC;gBACpB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,mBAAmB,GAAG,CAAC,MAAM,GAAG;gBAC9D,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;gBAC1B,cAAc,EAAE,SAAS,CAAC,MAAM,CAAC;gBACjC,IAAI,EAAE,MAAM;aACb,CAAC,CAAC;QACL,CAAC;QACD,OAAO,MAAW,CAAC;IACrB,CAAC;CACF;AA1tBD,oCA0tBC;AAED,mFAAmF;AACnF,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACzC,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,IAAa,EAAE,IAAY;IACxC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI;QAC9D,CAAC,CAAE,IAAgC,CAAC,IAAI,CAAC;QACzC,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC9B,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IACpC,4DAA4D;IAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,MAAM,CAAC,IAAa;IAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC9B,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAClE,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,8GAA8G;AAC9G,SAAS,UAAU,CAAC,CAQnB;IACC,OAAO;QACL,GAAG,CAAC,CAAC,MAAM;QACX,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;QACtC,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,YAAY,EAAE,CAAC,CAAC,YAAY;QAC5B,YAAY,EAAE,CAAC,CAAC,YAAY;KAC7B,CAAC;AACJ,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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"}
|