@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/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
- export declare function authorize(api: string, clientId: string, deps: AuthorizeDeps): Promise<AuthorizationCode>;
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, status, text) {
87
- // Fixed strings of our own — nothing from the query string is rendered.
88
- res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
89
- res.end(`<!doctype html><meta charset="utf-8"><title>Mapled</title>` +
90
- `<p style="font:16px system-ui,sans-serif;margin:40px">${text}</p>`);
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, 400, "This sign-in link doesn't belong to the running command.");
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, 200, "Sign-in was cancelled. You can close this tab.");
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, 400, "The sign-in link is missing its code.");
136
+ page(res, { kind: "incomplete" });
124
137
  return;
125
138
  }
126
- page(res, 200, "Signed in to Mapled. You can close this tab and return to the terminal.");
127
- finish(() => resolve({ code, verifier, redirectUri }));
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
- if (bf.relation.target !== af.relation.target) {
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
- if (bf.group.repeatable !== af.group.repeatable) {
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
- changes.push({ ...base, scope: "component", field: bs.key, op: "removed", detail: `removed — ${describeType(bs, names)}`, severity: "breaking" });
341
+ push(bs.key, "removed", `removed — ${describeType(bs, names)}`, "breaking");
329
342
  continue;
330
343
  }
331
- diffCommon(bs, as, names, (op, detail, severity) => changes.push({ ...base, scope: "component", field: bs.key, 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
- changes.push({ ...base, scope: "component", field: as.key, op: "added", detail: `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`, severity: "safe" });
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) {
@@ -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, a <template> counts only as the
9
- first one of a list or an each, a list or a record inside a repeated
10
- template is never read. The target is the sink the attribute names —
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. What the
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
  };