geneseed 3.1.1 → 3.1.3
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/CHANGELOG.md +123 -1
- package/DESIGN.md +15 -15
- package/README.md +1 -1
- package/SETUP.md +1 -1
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +39 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +111 -83
- package/docs/web/exclusions.md +1 -1
- package/docs/web/footprint.md +1 -1
- package/docs/web/foreman-mode.md +2 -2
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +41 -29
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +27 -28
- package/js/{render.mjs → build/render.mjs} +13 -9
- package/js/{checkout.mjs → build/source.mjs} +9 -9
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +18 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +63 -55
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +34 -32
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +84 -85
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +154 -51
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +21 -19
- package/js/web/routes.mjs +245 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-B49fE_ug.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-abt7hzD2.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-D3_bVkih.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-hXp7srsx.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-DvYMtlvl.js} +1 -1
- package/web/dist/assets/Harness-BIWei7N5.js +1 -0
- package/web/dist/assets/Laws-CxmnRdwt.js +3 -0
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-MjbOkHIW.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-Dx_w0faR.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-BRFvv2gr.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-CfsV1EVy.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DArA6IHS.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-C9bZOEZk.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-Cc0n2jwx.js} +1 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +1 -0
- package/web/dist/assets/index-DC3-GlCY.js +1 -0
- package/web/dist/assets/index-DP035wPa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-dHi-7M-F.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +5 -19
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1750
- package/js/emit.mjs +0 -1411
- package/web/dist/assets/About-DyK7yRy7.js +0 -1
- package/web/dist/assets/Harnesses-DgRyOviS.js +0 -1
- package/web/dist/assets/Laws-CWk3Mpri.js +0 -3
- package/web/dist/assets/Themes-CoAWuIeM.js +0 -2
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
- package/web/dist/assets/index-jrY4m9jP.js +0 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE WEB API ANSWERS — as a declaration, separate from the dispatcher that honours it.
|
|
3
|
+
*
|
|
4
|
+
* Eight exported sets and one map. They are not documentation: `tests/unit/web_server.mjs` reads
|
|
5
|
+
* them as a TWO-SIDED PARTITION and probes the real handler against them, because a declaration is
|
|
6
|
+
* not a dispatcher — mutation M23 exists precisely because a route can be declared in one and
|
|
7
|
+
* missing from the other, and nothing but a probe notices.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `server.mjs` because it is the half a reader consults to answer "does the console
|
|
10
|
+
* expose X?", which has nothing to do with sockets, daemons or gzip.
|
|
11
|
+
*/
|
|
12
|
+
import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
|
|
13
|
+
import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
|
|
14
|
+
import { apiActivityToggle } from './activity.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The API paths this phase does not answer yet.
|
|
18
|
+
*
|
|
19
|
+
* Without it every one of them would fall through to `serveStatic`, which answers ANY
|
|
20
|
+
* unknown path with index.html and a 200 — the SPA fallback. An unported endpoint would
|
|
21
|
+
* therefore return a plausible HTML page instead of failing, and the first cell written
|
|
22
|
+
* against it in P6b would be the thing that discovered the route was missing. A 501
|
|
23
|
+
* naming the phase costs one lookup and cannot be mistaken for a working endpoint.
|
|
24
|
+
*
|
|
25
|
+
* It shrank to empty in P6g and the DECLARATION STAYS. Every GET the reference answers is now
|
|
26
|
+
* answered here, so the two sets hold nothing — but they are half of a partition
|
|
27
|
+
* (`test_every_get_route_is_either_ported_or_declared_unported`), and deleting them would let
|
|
28
|
+
* the next GET route added to the reference fall through to the SPA with nothing to say so.
|
|
29
|
+
* An empty declaration is the partition asserting that there is nothing left to declare.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* SPLIT BY VERB SINCE P6b, and the split is the point rather than a tidy-up. Five paths
|
|
33
|
+
* answer BOTH verbs with different bodies — `/api/mcp`, `/api/excludes`, `/api/rules`,
|
|
34
|
+
* `/api/profile`, `/api/activity` — and P6b ports the GET half of two of them. One set
|
|
35
|
+
* keyed on path alone would have taken `/api/excludes` out of the unported list the
|
|
36
|
+
* moment its GET landed, and its POST would then have answered `{"error": "not found"}`
|
|
37
|
+
* with a 404: a plausible-looking response where the reference returns 200 or 409, which
|
|
38
|
+
* is exactly the failure the 501 exists to prevent.
|
|
39
|
+
*/
|
|
40
|
+
/** EMPTY SINCE P6g — every GET the reference answers is answered here. */
|
|
41
|
+
export const NOT_PORTED = new Set([]);
|
|
42
|
+
export const NOT_PORTED_PREFIXES = [];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The POSTs that have not crossed YET — and EMPTY SINCE P6i, which took `/api/install`.
|
|
46
|
+
*
|
|
47
|
+
* IT STAYS DECLARED, for the same reason the GET pair above stays and the prefix lists have
|
|
48
|
+
* stayed since P6g: this set is half of a partition
|
|
49
|
+
* (`test_every_post_route_is_either_ported_or_declared_unported`), so an empty set is the
|
|
50
|
+
* partition ASSERTING that there is nothing left to declare. Delete it and the next POST route
|
|
51
|
+
* added to the reference falls through to `{"error": "not found"}` at 404 — a plausible-looking
|
|
52
|
+
* answer where the reference returns 200 or 409 — with nothing to say so.
|
|
53
|
+
*
|
|
54
|
+
* THE DISPATCHER PROBE NEEDS A TARGET, which an empty set no longer supplies.
|
|
55
|
+
* `tests/test_web_server.py` retired its `unportedGet` probe when `NOT_PORTED` emptied in P6g;
|
|
56
|
+
* `unportedPost` is retired here for the same reason, and `declinedPost` (`/api/pick-folder`,
|
|
57
|
+
* which never crosses) is what keeps the 501 arm of the dispatcher under a live probe.
|
|
58
|
+
*/
|
|
59
|
+
export const NOT_PORTED_POST = new Set([]);
|
|
60
|
+
export const NOT_PORTED_POST_PREFIXES = [];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The POSTs that will NEVER cross, which is a different claim from the set above.
|
|
64
|
+
*
|
|
65
|
+
* `/api/pick-folder` opens an OS-NATIVE folder chooser on the daemon host — `osascript` on
|
|
66
|
+
* macOS, a one-shot `tkinter` subprocess elsewhere. There is no Node twin that is not a new
|
|
67
|
+
* GUI dependency, and reaching for one would also be a sixth `_ALLOWED_SPAWNS` row for a
|
|
68
|
+
* modal dialog. The UI already falls back to its editable path field when this endpoint
|
|
69
|
+
* fails, which is what a 501 gives it.
|
|
70
|
+
*
|
|
71
|
+
* SEPARATE FROM `NOT_PORTED_POST` ON PURPOSE. That set is a to-do list every later phase
|
|
72
|
+
* shrinks, and folding a permanent decline into it would make the list wrong in the one
|
|
73
|
+
* direction nobody checks: a phase that emptied it would have to either port a GUI dialog
|
|
74
|
+
* or quietly delete a declaration. Both sets are unioned into the partition the reference's
|
|
75
|
+
* routes are cross-checked against, so neither can drift.
|
|
76
|
+
*/
|
|
77
|
+
export const DECLINED_POST = new Set([
|
|
78
|
+
'/api/pick-folder',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The POSTs P6f answers — and this table IS the dispatch, not a declaration beside it.
|
|
83
|
+
*
|
|
84
|
+
* M23's lesson, applied one verb over: `test_the_declared_partition_is_the_one_the_dispatcher_uses`
|
|
85
|
+
* exists because a hand-kept list can agree with the reference while the running handler
|
|
86
|
+
* does something else. `doPost` looks routes up HERE, and `PORTED_POST` is this table's own
|
|
87
|
+
* keys, so the two cannot drift — the same reason `STATE_ROUTES` is a table on the GET side.
|
|
88
|
+
*
|
|
89
|
+
* THE SECOND COLUMN IS THE 409 CONVENTION, and it is a column because it is not uniform.
|
|
90
|
+
* `ok: false` means the write did not happen — a stale fingerprint, a body that never
|
|
91
|
+
* parsed, nothing to remove — and the client must re-fetch rather than retry, which is what
|
|
92
|
+
* 409 says and 200 does not. Five routes map it. `/api/activity` does NOT: its flag write
|
|
93
|
+
* answers 200 whatever `ok` says, and a port that unified the rule would be wrong exactly
|
|
94
|
+
* there. `/api/memory/delete` carries no `ok` at all — it deletes, or it raises `NotFound`
|
|
95
|
+
* and the outer catch answers 404 — so it is `false` here for a third reason again.
|
|
96
|
+
*
|
|
97
|
+
* `NotFound` IS NOT CAUGHT PER ROUTE. The reference wraps six of these calls in their own
|
|
98
|
+
* `except NotFound` AND wraps the whole of `_post_routes` in another inside `do_POST`. The
|
|
99
|
+
* inner six are redundant — every one produces the same `{"error": "not found: …"}` at 404
|
|
100
|
+
* — so the twin keeps the outer one only, in `handler`, where `do_POST`'s is.
|
|
101
|
+
*/
|
|
102
|
+
export const POST_ROUTES = new Map([
|
|
103
|
+
['/api/mcp', [apiMcpToggle, true]],
|
|
104
|
+
// P6i. `_post_routes` wraps this one in its own `except NotFound` and then maps
|
|
105
|
+
// `ok: false` to 409 — the same shape as `/api/mcp` beside it, so it fits the table's
|
|
106
|
+
// second column exactly and needs no third inline dispatch. The inner catch is redundant
|
|
107
|
+
// (see this table's docblock); `handler`'s outer one answers identically.
|
|
108
|
+
['/api/install', [apiInstallToggle, true]],
|
|
109
|
+
['/api/excludes', [apiExcludesMutate, true]],
|
|
110
|
+
['/api/view', [apiSelectView, true]],
|
|
111
|
+
['/api/activity', [apiActivityToggle, false]],
|
|
112
|
+
// `(self._read_json_body().get("name") or "")` — the SHELL resolves the name, and the
|
|
113
|
+
// endpoint takes a bare slug rather than a body.
|
|
114
|
+
['/api/memory/delete',
|
|
115
|
+
[(state, b) => apiMemoryDelete(state, isTruthy(b.name) ? formatValue(b.name) : ''), false]],
|
|
116
|
+
['/api/rules', [apiRulesMutate, true]],
|
|
117
|
+
['/api/rules/promote', [apiRulesPromote, true]],
|
|
118
|
+
['/api/profile', [apiProfileSave, true]],
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
export const PORTED_POST = [...POST_ROUTES.keys()];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The 409 column, exported so it can be cross-checked against the reference's own `200 if
|
|
125
|
+
* res.get("ok") else 409` conditionals by `ast`.
|
|
126
|
+
*
|
|
127
|
+
* A MUTATION IS WHY THIS EXISTS. Giving `/api/activity` the 409 treatment survived the whole
|
|
128
|
+
* cell matrix: every toggle a cell can perform succeeds, and reaching the `ok: false` arm
|
|
129
|
+
* needs the flag write to raise — which the two runtimes word differently, so no byte
|
|
130
|
+
* comparison could hold it. The column is the thing under test and no cell can see it, so
|
|
131
|
+
* the gate reads it out of both implementations instead.
|
|
132
|
+
*/
|
|
133
|
+
export const POST_ROUTES_CONVENTION = Object.fromEntries(
|
|
134
|
+
[...POST_ROUTES].map(([p, [, okIs409]]) => [p, okIs409]),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The GET paths the dispatcher answers OUTSIDE the two tables, declared so the partition
|
|
139
|
+
* cross-check can see them: `/api/ping` is the shell's own, and the two Docs routes take
|
|
140
|
+
* the `?harness=` query param, which a `(state)` table entry has no way to receive.
|
|
141
|
+
*
|
|
142
|
+
* A declaration is not a dispatcher — M23 — so
|
|
143
|
+
* `test_the_declared_partition_is_the_one_the_dispatcher_uses` probes all three against
|
|
144
|
+
* the running handler as well.
|
|
145
|
+
*/
|
|
146
|
+
export const PORTED_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
|
|
147
|
+
'/api/jobs', '/api/jobs/'];
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The POSTs P6g answers outside `POST_ROUTES`, and they are outside it for a reason the
|
|
151
|
+
* table's SECOND COLUMN makes plain.
|
|
152
|
+
*
|
|
153
|
+
* `POST_ROUTES` maps a route to `(fn, okIs409)` — one handler taking `(state, body)`, one
|
|
154
|
+
* boolean deciding 200 against 409. The jobs routes fit neither half. `/api/jobs/<id>/cancel`
|
|
155
|
+
* answers 200 or 404 on a lookup, not on an `ok` field; `/api/actions/<x>` answers 202 with a
|
|
156
|
+
* job id, 409 when the runner is busy, 404 for an action the table does not name, 501 for one
|
|
157
|
+
* that has not crossed, 400 or 409 for a resolver's refusal, and 200 for `restore` — six
|
|
158
|
+
* statuses over one prefix, none of them read off `ok`. Bending the column to fit would make
|
|
159
|
+
* it lie about the five routes it currently describes exactly, so these dispatch beside the
|
|
160
|
+
* table and `tests/test_web_jobs.py` reads them out of `PORTED_POST_INLINE` instead.
|
|
161
|
+
*/
|
|
162
|
+
export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
|
|
163
|
+
'/api/actions/'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* POST routes THAT NEVER EXISTED ON THE REFERENCE — the first of them, and the reason this set
|
|
167
|
+
* has to exist at all.
|
|
168
|
+
*
|
|
169
|
+
* `test_every_post_route_is_either_ported_or_declared_unported` asserts SET EQUALITY against
|
|
170
|
+
* `REF_POST`, a frozen literal of the paths the Python daemon answered. That was exactly right
|
|
171
|
+
* while the port was in flight: a route on one side and not the other was a defect either way.
|
|
172
|
+
* The port is finished and the reference is deleted, so the assertion now says something else —
|
|
173
|
+
* "this daemon may never grow a route" — which is a rule nobody chose.
|
|
174
|
+
*
|
|
175
|
+
* SO THE PARTITION GAINS A FOURTH PART RATHER THAN LOSING ITS EQUALITY. Widening `REF_POST` would
|
|
176
|
+
* have been the cheap edit and it would have been a lie: that list is a RECORD of what the
|
|
177
|
+
* reference answered, and a route added in 2026 was never in it. Keeping the record honest and
|
|
178
|
+
* declaring the additions beside it keeps both claims checkable — the reference's surface is
|
|
179
|
+
* still pinned, and everything past it is enumerated here instead of merely appearing.
|
|
180
|
+
*
|
|
181
|
+
* A route belongs here only once it is dispatched below; the dispatcher probe is what keeps this
|
|
182
|
+
* from becoming a declaration nobody honours.
|
|
183
|
+
*/
|
|
184
|
+
export const POST_BEYOND_REF = new Set([
|
|
185
|
+
'/api/reveal',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* GET routes THAT NEVER EXISTED ON THE REFERENCE — the same fourth part of the partition, one
|
|
190
|
+
* verb over, and it exists for the reason `POST_BEYOND_REF` above spells out in full.
|
|
191
|
+
*
|
|
192
|
+
* `test_every_get_route_is_either_ported_or_declared_unported` asserts SET EQUALITY against
|
|
193
|
+
* `REF_GET`, a frozen literal of the paths the Python daemon answered. Widening that list to
|
|
194
|
+
* admit a route added in 2026 would forge the record; leaving the equality unqualified would
|
|
195
|
+
* mean this daemon may never grow a GET. So additions are enumerated here instead, and the
|
|
196
|
+
* gate compares the covered set against the reference's surface PLUS this one.
|
|
197
|
+
*
|
|
198
|
+
* `/api/recent` is the first, and it is a genuine gap rather than a convenience: no catalog row
|
|
199
|
+
* on the whole surface carries a date, and a browser cannot stat the `source` path it is handed
|
|
200
|
+
* — so "what changed lately" had no answer that did not involve the server looking. See
|
|
201
|
+
* `apiRecent` in `api.mjs` for what it will and will not date.
|
|
202
|
+
*
|
|
203
|
+
* A route belongs here only once it is dispatched; the dispatcher probe is what keeps this from
|
|
204
|
+
* becoming a declaration nobody honours.
|
|
205
|
+
*/
|
|
206
|
+
export const GET_BEYOND_REF = new Set([
|
|
207
|
+
'/api/recent',
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
export function notPorted(path) {
|
|
211
|
+
return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function notPortedPost(path) {
|
|
215
|
+
return NOT_PORTED_POST.has(path) || DECLINED_POST.has(path)
|
|
216
|
+
|| NOT_PORTED_POST_PREFIXES.some((p) => path.startsWith(p));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `Handler._read_json_body` — the drained body as a dict, `{}` for ANYTHING else.
|
|
221
|
+
*
|
|
222
|
+
* P6a retained the drain with a note that it was measured indistinguishable and stayed
|
|
223
|
+
* because "`_read_json_body` needs the bytes from P6f on". This is that caller.
|
|
224
|
+
*
|
|
225
|
+
* `{}` AND NEVER A RAISE is the contract, and it is what makes a malformed body a 409 or a
|
|
226
|
+
* 404 from the endpoint's own validation rather than a 500 from a parser. That matters
|
|
227
|
+
* beyond tidiness: P5a's one un-portable line is a `json` error message, because CPython
|
|
228
|
+
* and V8 word them differently AND report different offsets — so a 500 carrying a parse
|
|
229
|
+
* error is a response the two implementations can never agree on.
|
|
230
|
+
*
|
|
231
|
+
* `parseJson`, not `JSON.parse`: a body value can be written straight into a user's file
|
|
232
|
+
* (`api_profile_save`'s `text`, `api_rules_mutate`'s `title`) and `str(1.0)` is `"1.0"`
|
|
233
|
+
* where `String(1)` is `"1"`. A non-object body — a bare list, a number, `null` — takes the
|
|
234
|
+
* same `{}` arm as garbage, because `isinstance(obj, dict)` is what the reference asks.
|
|
235
|
+
*/
|
|
236
|
+
export function readJsonBody(buf) {
|
|
237
|
+
let obj;
|
|
238
|
+
try {
|
|
239
|
+
obj = parseJson((buf && buf.length ? buf : Buffer.from('{}')).toString('utf-8'));
|
|
240
|
+
} catch {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
return (obj !== null && typeof obj === 'object' && obj.constructor === Object) ? obj : {};
|
|
244
|
+
}
|
|
245
|
+
|