@mapled/cli 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/callbackPage.d.ts +50 -0
- package/dist/callbackPage.js +141 -0
- package/dist/commands.js +29 -14
- package/dist/doctor.d.ts +2 -0
- package/dist/logo.d.ts +4 -0
- package/dist/logo.js +4 -0
- package/dist/manifest.d.ts +34 -7
- package/dist/manifest.js +86 -18
- package/dist/media.js +5 -3
- package/dist/oauth.d.ts +14 -2
- package/dist/oauth.js +34 -11
- package/dist/pin.js +63 -43
- package/dist/scanHtml.d.ts +11 -4
- package/dist/scanHtml.js +343 -35
- package/dist/schema.d.ts +17 -1
- package/dist/schema.js +20 -0
- package/dist/types.js +25 -5
- package/dist/vanillaDigests.js +1 -0
- package/package.json +1 -1
package/dist/oauth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CallbackView } from "./callbackPage.js";
|
|
1
2
|
/** The CLI signs in as an OAuth 2.1 public client of the Mapled API —
|
|
2
3
|
the same authorization server the remote MCP uses (S-54): dynamic
|
|
3
4
|
registration, PKCE, consent on app.mapled.io, a loopback redirect
|
|
@@ -17,6 +18,13 @@ export type AuthorizationCode = {
|
|
|
17
18
|
verifier: string;
|
|
18
19
|
redirectUri: string;
|
|
19
20
|
};
|
|
21
|
+
/** The browser tab that brought the code waits on this: the caller says
|
|
22
|
+
how the sign-in ended once it knows (the project, where to go on). */
|
|
23
|
+
export type SignInTab = {
|
|
24
|
+
show: (view: CallbackView) => void;
|
|
25
|
+
};
|
|
26
|
+
/** How long the tab waits for `show` before it hands over on its own. */
|
|
27
|
+
export declare const HOLD_MS = 30000;
|
|
20
28
|
export declare function pkce(): {
|
|
21
29
|
verifier: string;
|
|
22
30
|
challenge: string;
|
|
@@ -39,8 +47,12 @@ export type AuthorizeDeps = {
|
|
|
39
47
|
/** The authorization code flow with a loopback redirect: a one-off
|
|
40
48
|
server on 127.0.0.1 receives the code once the person approves on
|
|
41
49
|
app.mapled.io. Rejects on denial, on timeout, and never on a state
|
|
42
|
-
that isn't this run's.
|
|
43
|
-
|
|
50
|
+
that isn't this run's. The tab that brought the code is answered by
|
|
51
|
+
the caller through `tab.show` — after the exchange, when the project
|
|
52
|
+
is known — or, failing that, after `HOLD_MS`. */
|
|
53
|
+
export declare function authorize(api: string, clientId: string, deps: AuthorizeDeps): Promise<AuthorizationCode & {
|
|
54
|
+
tab: SignInTab;
|
|
55
|
+
}>;
|
|
44
56
|
export declare function exchangeCode(api: string, clientId: string, grant: AuthorizationCode, f: Fetch): Promise<TokenSet>;
|
|
45
57
|
export declare function refreshTokens(api: string, clientId: string, refreshToken: string, f: Fetch): Promise<TokenSet>;
|
|
46
58
|
/** Ends the connection (RFC 7009). Best effort — the local copy goes either way. */
|
package/dist/oauth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
+
import { callbackPage } from "./callbackPage.js";
|
|
3
4
|
import { CliError } from "./errors.js";
|
|
4
5
|
/** The CLI signs in as an OAuth 2.1 public client of the Mapled API —
|
|
5
6
|
the same authorization server the remote MCP uses (S-54): dynamic
|
|
@@ -9,6 +10,8 @@ import { CliError } from "./errors.js";
|
|
|
9
10
|
export const CLIENT_NAME = "Mapled CLI";
|
|
10
11
|
export const REDIRECT_PATH = "/callback";
|
|
11
12
|
export const SCOPE = "mapled";
|
|
13
|
+
/** How long the tab waits for `show` before it hands over on its own. */
|
|
14
|
+
export const HOLD_MS = 30_000;
|
|
12
15
|
export function pkce() {
|
|
13
16
|
const verifier = randomBytes(48).toString("base64url");
|
|
14
17
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -83,16 +86,26 @@ export async function clientKnown(api, clientId, f) {
|
|
|
83
86
|
return false;
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
|
-
function page(res,
|
|
87
|
-
// Fixed strings of our own
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
function page(res, view) {
|
|
90
|
+
// Fixed strings of our own and the project's name, escaped — nothing
|
|
91
|
+
// from the query string is rendered.
|
|
92
|
+
const { status, html, csp } = callbackPage(view);
|
|
93
|
+
res.writeHead(status, {
|
|
94
|
+
"content-type": "text/html; charset=utf-8",
|
|
95
|
+
"cache-control": "no-store",
|
|
96
|
+
"content-security-policy": csp,
|
|
97
|
+
"referrer-policy": "no-referrer",
|
|
98
|
+
"x-content-type-options": "nosniff",
|
|
99
|
+
connection: "close",
|
|
100
|
+
});
|
|
101
|
+
res.end(html);
|
|
91
102
|
}
|
|
92
103
|
/** The authorization code flow with a loopback redirect: a one-off
|
|
93
104
|
server on 127.0.0.1 receives the code once the person approves on
|
|
94
105
|
app.mapled.io. Rejects on denial, on timeout, and never on a state
|
|
95
|
-
that isn't this run's.
|
|
106
|
+
that isn't this run's. The tab that brought the code is answered by
|
|
107
|
+
the caller through `tab.show` — after the exchange, when the project
|
|
108
|
+
is known — or, failing that, after `HOLD_MS`. */
|
|
96
109
|
export function authorize(api, clientId, deps) {
|
|
97
110
|
const { verifier, challenge } = pkce();
|
|
98
111
|
const state = randomBytes(16).toString("base64url");
|
|
@@ -107,12 +120,12 @@ export function authorize(api, clientId, deps) {
|
|
|
107
120
|
}
|
|
108
121
|
if (url.searchParams.get("state") !== state) {
|
|
109
122
|
// Not this run's redirect — keep waiting for the real one.
|
|
110
|
-
page(res,
|
|
123
|
+
page(res, { kind: "expired" });
|
|
111
124
|
return;
|
|
112
125
|
}
|
|
113
126
|
const error = url.searchParams.get("error");
|
|
114
127
|
if (error) {
|
|
115
|
-
page(res,
|
|
128
|
+
page(res, { kind: "cancelled" });
|
|
116
129
|
finish(() => reject(new CliError(error === "access_denied"
|
|
117
130
|
? "Sign-in was denied in the browser."
|
|
118
131
|
: `Sign-in failed: ${url.searchParams.get("error_description") ?? error}`)));
|
|
@@ -120,11 +133,21 @@ export function authorize(api, clientId, deps) {
|
|
|
120
133
|
}
|
|
121
134
|
const code = url.searchParams.get("code");
|
|
122
135
|
if (!code) {
|
|
123
|
-
page(res,
|
|
136
|
+
page(res, { kind: "incomplete" });
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
// the tab waits for the outcome; the server stops listening now
|
|
140
|
+
let shown = false;
|
|
141
|
+
const show = (view) => {
|
|
142
|
+
if (shown)
|
|
143
|
+
return;
|
|
144
|
+
shown = true;
|
|
145
|
+
clearTimeout(hold);
|
|
146
|
+
page(res, view);
|
|
147
|
+
};
|
|
148
|
+
const hold = setTimeout(() => show({ kind: "handed-over" }), HOLD_MS);
|
|
149
|
+
hold.unref?.();
|
|
150
|
+
finish(() => resolve({ code, verifier, redirectUri, tab: { show } }));
|
|
128
151
|
});
|
|
129
152
|
const timer = setTimeout(() => finish(() => reject(new CliError("Timed out waiting for the browser. Run `mapled auth login` again."))), deps.timeoutMs ?? 5 * 60_000);
|
|
130
153
|
function finish(then) {
|
package/dist/pin.js
CHANGED
|
@@ -14,6 +14,14 @@ function pinSubField(s) {
|
|
|
14
14
|
out.helpText = s.helpText ?? null;
|
|
15
15
|
out.options = s.options ?? null;
|
|
16
16
|
out.sensitive = s.sensitive === true;
|
|
17
|
+
// a component type's group sub-field (§45.4) is in the schema hash: only it carries one
|
|
18
|
+
if (s.type === "group" && s.group) {
|
|
19
|
+
out.group = { repeatable: s.group.repeatable, ...(s.group.maxItems ? { maxItems: s.group.maxItems } : {}), fields: s.group.fields.map(pinSubField) };
|
|
20
|
+
}
|
|
21
|
+
// and its relation sub-field's collection (§45.4)
|
|
22
|
+
if (s.type === "relation" && s.relation) {
|
|
23
|
+
out.relation = { target: s.relation.target, cardinality: s.relation.cardinality, ...(s.relation.onDelete ? { onDelete: s.relation.onDelete } : {}) };
|
|
24
|
+
}
|
|
17
25
|
return out;
|
|
18
26
|
}
|
|
19
27
|
function pinField(f) {
|
|
@@ -157,6 +165,47 @@ function diffCommon(before, after, names, push) {
|
|
|
157
165
|
push("changed", "no longer sensitive — delivered to the site now", "safe");
|
|
158
166
|
}
|
|
159
167
|
}
|
|
168
|
+
/** A group's own changes — one item or a list, its limit, its
|
|
169
|
+
sub-fields removed, changed and added — the sub-fields under
|
|
170
|
+
`under.sub`. */
|
|
171
|
+
function diffGroup(before, after, under, names, push) {
|
|
172
|
+
if (before.repeatable !== after.repeatable) {
|
|
173
|
+
push(under, "changed", before.repeatable ? "repeatable → single item" : "single item → repeatable", "breaking");
|
|
174
|
+
}
|
|
175
|
+
if ((before.maxItems ?? null) !== (after.maxItems ?? null)) {
|
|
176
|
+
const limit = (n) => (n ? `up to ${n} items` : "no item limit");
|
|
177
|
+
push(under, "changed", `${limit(before.maxItems)} → ${limit(after.maxItems)}`, "safe");
|
|
178
|
+
}
|
|
179
|
+
const afterSubs = new Map(after.fields.map((s) => [s.key, s]));
|
|
180
|
+
const beforeSubs = new Map(before.fields.map((s) => [s.key, s]));
|
|
181
|
+
for (const bs of before.fields) {
|
|
182
|
+
const as = afterSubs.get(bs.key);
|
|
183
|
+
const sub = `${under}.${bs.key}`;
|
|
184
|
+
if (!as) {
|
|
185
|
+
push(sub, "removed", `removed — ${describeType(bs, names)}`, "breaking");
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
diffCommon(bs, as, names, (op, detail, severity) => push(sub, op, detail, severity));
|
|
189
|
+
}
|
|
190
|
+
for (const as of after.fields) {
|
|
191
|
+
if (beforeSubs.has(as.key))
|
|
192
|
+
continue;
|
|
193
|
+
push(`${under}.${as.key}`, "added", `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`, "safe");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** A relation's own changes — a field's, or a block's relation
|
|
197
|
+
sub-field's (§45.4): where it links and how many are breaking, what a
|
|
198
|
+
delete does is safe. */
|
|
199
|
+
function diffRelation(before, after, names, push) {
|
|
200
|
+
if (before.target !== after.target) {
|
|
201
|
+
push("changed", `relation to ${names.get(before.target) ?? before.target} → ${names.get(after.target) ?? after.target}`, "breaking");
|
|
202
|
+
}
|
|
203
|
+
if (before.cardinality !== after.cardinality)
|
|
204
|
+
push("changed", `${before.cardinality} → ${after.cardinality}`, "breaking");
|
|
205
|
+
if ((before.onDelete ?? null) !== (after.onDelete ?? null)) {
|
|
206
|
+
push("changed", `on delete ${before.onDelete ?? "default"} → ${after.onDelete ?? "default"}`, "safe");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
160
209
|
function sameJson(a, b) {
|
|
161
210
|
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
162
211
|
}
|
|
@@ -213,16 +262,8 @@ export function diffSchema(before, after, componentTypes = {}) {
|
|
|
213
262
|
if ((bf.localized === true) !== (af.localized === true)) {
|
|
214
263
|
push("changed", af.localized === true ? "now translated — the site still reads the default language" : "no longer translated — the default language's value stays", "safe");
|
|
215
264
|
}
|
|
216
|
-
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation)
|
|
217
|
-
|
|
218
|
-
push("changed", `relation to ${names.get(bf.relation.target) ?? bf.relation.target} → ${names.get(af.relation.target) ?? af.relation.target}`, "breaking");
|
|
219
|
-
}
|
|
220
|
-
if (bf.relation.cardinality !== af.relation.cardinality)
|
|
221
|
-
push("changed", `${bf.relation.cardinality} → ${af.relation.cardinality}`, "breaking");
|
|
222
|
-
if ((bf.relation.onDelete ?? null) !== (af.relation.onDelete ?? null)) {
|
|
223
|
-
push("changed", `on delete ${bf.relation.onDelete ?? "default"} → ${af.relation.onDelete ?? "default"}`, "safe");
|
|
224
|
-
}
|
|
225
|
-
}
|
|
265
|
+
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation)
|
|
266
|
+
diffRelation(bf.relation, af.relation, names, push);
|
|
226
267
|
if (bf.type === "computed" && af.type === "computed" && bf.computed && af.computed) {
|
|
227
268
|
if (bf.computed.resultType !== af.computed.resultType) {
|
|
228
269
|
push("changed", `${describeType(bf, names)} → ${describeType(af, names)}`, "breaking");
|
|
@@ -232,36 +273,7 @@ export function diffSchema(before, after, componentTypes = {}) {
|
|
|
232
273
|
}
|
|
233
274
|
}
|
|
234
275
|
if (bf.type === "group" && af.type === "group" && bf.group && af.group) {
|
|
235
|
-
|
|
236
|
-
push("changed", bf.group.repeatable ? "repeatable → single item" : "single item → repeatable", "breaking");
|
|
237
|
-
}
|
|
238
|
-
if ((bf.group.maxItems ?? null) !== (af.group.maxItems ?? null)) {
|
|
239
|
-
const limit = (n) => (n ? `up to ${n} items` : "no item limit");
|
|
240
|
-
push("changed", `${limit(bf.group.maxItems)} → ${limit(af.group.maxItems)}`, "safe");
|
|
241
|
-
}
|
|
242
|
-
const afterSubs = new Map(af.group.fields.map((s) => [s.key, s]));
|
|
243
|
-
const beforeSubs = new Map(bf.group.fields.map((s) => [s.key, s]));
|
|
244
|
-
for (const bs of bf.group.fields) {
|
|
245
|
-
const as = afterSubs.get(bs.key);
|
|
246
|
-
const sub = `${under}.${bs.key}`;
|
|
247
|
-
if (!as) {
|
|
248
|
-
changes.push({ ...base, scope: "subfield", field: sub, op: "removed", detail: `removed — ${describeType(bs, names)}`, severity: "breaking" });
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
diffCommon(bs, as, names, (op, detail, severity) => changes.push({ ...base, scope: "subfield", field: sub, op, detail, severity }));
|
|
252
|
-
}
|
|
253
|
-
for (const as of af.group.fields) {
|
|
254
|
-
if (beforeSubs.has(as.key))
|
|
255
|
-
continue;
|
|
256
|
-
changes.push({
|
|
257
|
-
...base,
|
|
258
|
-
scope: "subfield",
|
|
259
|
-
field: `${under}.${as.key}`,
|
|
260
|
-
op: "added",
|
|
261
|
-
detail: `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`,
|
|
262
|
-
severity: "safe",
|
|
263
|
-
});
|
|
264
|
-
}
|
|
276
|
+
diffGroup(bf.group, af.group, under, names, (field, op, detail, severity) => changes.push({ ...base, scope: field === under ? "field" : "subfield", field, op, detail, severity }));
|
|
265
277
|
}
|
|
266
278
|
// §45.4: a type the list may no longer hold leaves the site's union; a new one is an item the site doesn't render yet
|
|
267
279
|
if (bf.type === "components" && af.type === "components" && bf.components && af.components) {
|
|
@@ -322,18 +334,26 @@ export function diffSchema(before, after, componentTypes = {}) {
|
|
|
322
334
|
changes.push({ ...base, scope: "component", op: "changed", detail: `name ${q(bt.displayName)} → ${q(at.displayName)}`, severity: "safe" });
|
|
323
335
|
const afterSubs = new Map(at.fields.map((s) => [s.key, s]));
|
|
324
336
|
const beforeSubs = new Map(bt.fields.map((s) => [s.key, s]));
|
|
337
|
+
const push = (field, op, detail, severity) => changes.push({ ...base, scope: "component", field, op, detail, severity });
|
|
325
338
|
for (const bs of bt.fields) {
|
|
326
339
|
const as = afterSubs.get(bs.key);
|
|
327
340
|
if (!as) {
|
|
328
|
-
|
|
341
|
+
push(bs.key, "removed", `removed — ${describeType(bs, names)}`, "breaking");
|
|
329
342
|
continue;
|
|
330
343
|
}
|
|
331
|
-
diffCommon(bs, as, names, (op, detail, severity) =>
|
|
344
|
+
diffCommon(bs, as, names, (op, detail, severity) => push(bs.key, op, detail, severity));
|
|
345
|
+
// a group sub-field's items (§45.4): its own sub-fields under `tiers.name`
|
|
346
|
+
if (bs.type === "group" && as.type === "group" && bs.group && as.group)
|
|
347
|
+
diffGroup(bs.group, as.group, bs.key, names, push);
|
|
348
|
+
// a relation sub-field's collection and cardinality, as a relation field's
|
|
349
|
+
if (bs.type === "relation" && as.type === "relation" && bs.relation && as.relation) {
|
|
350
|
+
diffRelation(bs.relation, as.relation, names, (op, detail, severity) => push(bs.key, op, detail, severity));
|
|
351
|
+
}
|
|
332
352
|
}
|
|
333
353
|
for (const as of at.fields) {
|
|
334
354
|
if (beforeSubs.has(as.key))
|
|
335
355
|
continue;
|
|
336
|
-
|
|
356
|
+
push(as.key, "added", `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`, "safe");
|
|
337
357
|
}
|
|
338
358
|
}
|
|
339
359
|
for (const at of typesAfter) {
|
package/dist/scanHtml.d.ts
CHANGED
|
@@ -5,16 +5,23 @@ import type { Schema } from "./schema.js";
|
|
|
5
5
|
`data-mapled-*` attributes declare — read the way the runtime reads
|
|
6
6
|
them (packages/vanilla/src/runtime.ts, `compile`): a path starts at a
|
|
7
7
|
single from the document and at the record inside a list or a record,
|
|
8
|
-
an each repeats the items of a group
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
an each repeats the items of a group or the blocks of a components
|
|
9
|
+
field — inside a block's template, the items of its group sub-field
|
|
10
|
+
(§45.4) — a list's records and a group's items take the first <template>
|
|
11
|
+
without data-mapled-type and a block the first of its type (else that
|
|
12
|
+
one), a list or a record inside a repeated template is never read. The target is the sink the attribute names —
|
|
11
13
|
`markdown` renders rich text, an <img> an image, `bind-href` a link —
|
|
12
|
-
so a field bound where it can't render shows as a mismatch
|
|
14
|
+
so a field bound where it can't render shows as a mismatch; a place
|
|
15
|
+
the runtime doesn't write into (a code element, an attribute off its
|
|
16
|
+
elements, an attribute inside <svg>) binds nothing and earns a note. What the
|
|
13
17
|
runtime can't tell apart — an empty field and a misspelled key — the
|
|
14
18
|
scan can: it has the schema. Nothing on the page runs or leaves the
|
|
15
19
|
machine; only the manifest does, on `bindings push`. */
|
|
20
|
+
/** `html`: the element is in the HTML namespace — not SVG or MathML, where
|
|
21
|
+
the runtime writes no attribute (DOM rule 3). */
|
|
16
22
|
export type HtmlElement = {
|
|
17
23
|
name: string;
|
|
24
|
+
html: boolean;
|
|
18
25
|
attrs: Map<string, string>;
|
|
19
26
|
children: HtmlElement[];
|
|
20
27
|
};
|