slidemind 0.3.20 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -8,7 +8,7 @@ import https from "node:https";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { deriveEnvLabel, envPhrase, getSessionFor, readSessions } from "../config.mjs";
|
|
11
|
-
import { ensureSignedIn, getGraphToken, getSharePointToken } from "../graph-auth.mjs";
|
|
11
|
+
import { ensureSignedIn, getGraphToken, getSharePointToken, tokenRefreshFailure } from "../graph-auth.mjs";
|
|
12
12
|
import { updateCliInstruction } from "../../install-channel.mjs";
|
|
13
13
|
|
|
14
14
|
// This CLI's own version, for the add-in's minCliVersion floor (see
|
|
@@ -184,8 +184,7 @@ export function isTransientBridgeError(err) {
|
|
|
184
184
|
return /relay unreachable/i.test(String(err.message || ""));
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
export async function withTransientRetry(fn, isRetryable = isTransientBridgeError) {
|
|
188
|
-
const delays = transientRetryDelays();
|
|
187
|
+
export async function withTransientRetry(fn, isRetryable = isTransientBridgeError, delays = transientRetryDelays()) {
|
|
189
188
|
for (let attempt = 0; ; attempt += 1) {
|
|
190
189
|
try {
|
|
191
190
|
return await fn();
|
|
@@ -283,6 +282,7 @@ export async function bridgeFetch(pathPart, init = {}, backendURL = null) {
|
|
|
283
282
|
(detail ? `: ${detail}` : ""),
|
|
284
283
|
);
|
|
285
284
|
err.status = response.status;
|
|
285
|
+
err.headers = response.headers ?? {};
|
|
286
286
|
const code = machineCode ?? humanError;
|
|
287
287
|
if (code) err.code = code;
|
|
288
288
|
if (parsed) err.body = parsed;
|
|
@@ -499,6 +499,7 @@ export async function bridgeStreamToFile(pathPart, init, destPath, backendURL =
|
|
|
499
499
|
(detail ? `: ${detail}` : ""),
|
|
500
500
|
);
|
|
501
501
|
err.status = response.status;
|
|
502
|
+
err.headers = response.headers ?? {};
|
|
502
503
|
const code = machineCode ?? humanError;
|
|
503
504
|
if (code) err.code = code;
|
|
504
505
|
if (parsed) err.body = parsed;
|
|
@@ -516,40 +517,376 @@ export function sleep(ms) {
|
|
|
516
517
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
517
518
|
}
|
|
518
519
|
|
|
519
|
-
// One capability probe per backend
|
|
520
|
-
// URL (callers always resolve before probing), so
|
|
521
|
-
// mixed versions each get their own answer.
|
|
520
|
+
// One capability probe per backend, cached for CAPABILITIES_TTL_MS. Keyed by
|
|
521
|
+
// the RESOLVED backend URL (callers always resolve before probing), so
|
|
522
|
+
// multiple paired backends on mixed versions each get their own answer.
|
|
523
|
+
//
|
|
524
|
+
// Entries are `{probe, at}`: a short-lived CLI never notices the TTL, the
|
|
525
|
+
// long-lived workspace daemon does — a deployment can flip a policy flag
|
|
526
|
+
// while it runs. A queued remote MUTATION passes `refresh: true` and bypasses
|
|
527
|
+
// the cache outright; batch reads may take the cached answer.
|
|
528
|
+
export const CAPABILITIES_TTL_MS = 60_000;
|
|
522
529
|
export const backendCapabilitiesCache = new Map();
|
|
523
530
|
|
|
524
|
-
//
|
|
525
|
-
// `
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
export
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
531
|
+
// A probe that did not get an answer is tried again before it is reported:
|
|
532
|
+
// `CAPABILITY_PROBE_ATTEMPTS` requests in all, spaced by the server's
|
|
533
|
+
// `Retry-After` when it sent one (it does on 429) and by this backoff otherwise.
|
|
534
|
+
export const CAPABILITY_PROBE_ATTEMPTS = 3;
|
|
535
|
+
export const CAPABILITY_PROBE_BACKOFF_MS = [1_000, 2_000];
|
|
536
|
+
export const CAPABILITY_PROBE_MAX_DELAY_MS = 30_000;
|
|
537
|
+
|
|
538
|
+
// Seconds or an HTTP date, as `Retry-After` allows; null when absent/unusable.
|
|
539
|
+
function retryAfterDelayMs(err, now = Date.now()) {
|
|
540
|
+
const value = err?.headers?.["retry-after"];
|
|
541
|
+
if (value == null || value === "") return null;
|
|
542
|
+
const seconds = Number(value);
|
|
543
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
544
|
+
const date = Date.parse(String(value));
|
|
545
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// What one failed probe request says about the backend's capabilities:
|
|
549
|
+
// absent 404 — an add-in from before /api/cli/capabilities. That IS an
|
|
550
|
+
// answer: no capabilities, and asking again will not change it.
|
|
551
|
+
// unknown anything else — a timeout, a 5xx, a 429, an unreachable relay, a
|
|
552
|
+
// revoked session. Nothing was learned about the capabilities.
|
|
553
|
+
// Of the unknowns, two are not worth a second request: a 401 (the session is
|
|
554
|
+
// dead; the error already names the remediation) and the add-in's version floor
|
|
555
|
+
// (the answer is "update the CLI", however often it is asked).
|
|
556
|
+
export function probeOutcome(err) {
|
|
557
|
+
if (err?.status === 404) return "absent";
|
|
558
|
+
return "unknown";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function probeRetryable(err) {
|
|
562
|
+
if (probeOutcome(err) !== "unknown") return false;
|
|
563
|
+
if (isSessionInvalidError(err)) return false;
|
|
564
|
+
if (err?.code === "cli_update_required") return false;
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// The probe result, failure included, as `{ status, caps, error, attempts }`:
|
|
569
|
+
// answered `caps` is what the add-in advertises (`error` null)
|
|
570
|
+
// absent the add-in predates the route (`caps` {}, `error` the 404):
|
|
571
|
+
// no capabilities — sync render, no version floor, pane relay
|
|
572
|
+
// unknown the probe FAILED to get an answer (`caps` null, `error` the last
|
|
573
|
+
// failure). It is not an answer and is never read as one: a
|
|
574
|
+
// capability that could not be discovered is neither present nor
|
|
575
|
+
// absent. Callers that can act on the distinction read `status`;
|
|
576
|
+
// everyone else goes through `backendCapabilities`, which refuses
|
|
577
|
+
// with a typed, retryable error instead of handing back `{}`.
|
|
578
|
+
//
|
|
579
|
+
// Only ANSWERS are cached (answered and absent — both are facts about the
|
|
580
|
+
// deployment, and good for CAPABILITIES_TTL_MS). An unknown is retried here,
|
|
581
|
+
// shared with every caller that asks while it is in flight, and then dropped:
|
|
582
|
+
// caching it would turn one slow second into sixty seconds of every optional
|
|
583
|
+
// feature "missing" and every routing decision taking the wrong path (x103:
|
|
584
|
+
// one timed-out probe sent the icon fetch to the pane relay of a closed
|
|
585
|
+
// PowerPoint, and reported "No PowerPoint window" for it).
|
|
586
|
+
export function backendCapabilitiesResult(
|
|
587
|
+
backendURL,
|
|
588
|
+
{
|
|
589
|
+
refresh = false,
|
|
590
|
+
maxAgeMs = CAPABILITIES_TTL_MS,
|
|
591
|
+
attempts = CAPABILITY_PROBE_ATTEMPTS,
|
|
592
|
+
sleep: wait = sleep,
|
|
593
|
+
} = {},
|
|
594
|
+
) {
|
|
595
|
+
const cached = backendCapabilitiesCache.get(backendURL);
|
|
596
|
+
if (cached && !refresh && Date.now() - cached.at < maxAgeMs) return cached.probe;
|
|
597
|
+
const probe = (async () => {
|
|
598
|
+
let error = null;
|
|
599
|
+
for (let attempt = 1; ; attempt++) {
|
|
533
600
|
try {
|
|
534
601
|
const res = await bridgeFetch(
|
|
535
602
|
"/api/cli/capabilities",
|
|
536
603
|
{ timeoutMs: listTimeoutMs() },
|
|
537
604
|
backendURL,
|
|
538
605
|
);
|
|
539
|
-
return { caps: (await res.json()) ?? {}, error: null };
|
|
606
|
+
return { status: "answered", caps: (await res.json()) ?? {}, error: null, attempts: attempt };
|
|
540
607
|
} catch (err) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
608
|
+
error = err;
|
|
609
|
+
if (probeOutcome(err) === "absent") {
|
|
610
|
+
return { status: "absent", caps: {}, error: err, attempts: attempt };
|
|
611
|
+
}
|
|
612
|
+
const delay = retryAfterDelayMs(err) ?? CAPABILITY_PROBE_BACKOFF_MS[attempt - 1] ?? null;
|
|
613
|
+
if (!probeRetryable(err) || attempt >= attempts || delay === null || delay > CAPABILITY_PROBE_MAX_DELAY_MS) {
|
|
614
|
+
return { status: "unknown", caps: null, error: err, attempts: attempt };
|
|
615
|
+
}
|
|
616
|
+
await wait(delay);
|
|
544
617
|
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
}
|
|
618
|
+
}
|
|
619
|
+
})();
|
|
620
|
+
const entry = { probe, at: Date.now() };
|
|
621
|
+
backendCapabilitiesCache.set(backendURL, entry);
|
|
622
|
+
probe.then(
|
|
623
|
+
(result) => {
|
|
624
|
+
// Drop an unknown, but only this one: a fresher probe may have replaced it.
|
|
625
|
+
if (result.status === "unknown" && backendCapabilitiesCache.get(backendURL) === entry) {
|
|
626
|
+
backendCapabilitiesCache.delete(backendURL);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
() => {},
|
|
630
|
+
);
|
|
548
631
|
return probe;
|
|
549
632
|
}
|
|
550
633
|
|
|
634
|
+
export function refreshBackendCapabilities(backendURL) {
|
|
635
|
+
return backendCapabilitiesResult(backendURL, { refresh: true });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// The error every capability reader raises for an `unknown` probe: typed,
|
|
639
|
+
// retryable, and carrying the real cause — the status, headers (Retry-After)
|
|
640
|
+
// and message of the request that failed — so a caller's retry ladder treats
|
|
641
|
+
// it like the transport failure it is. A dead session keeps its own error:
|
|
642
|
+
// "reconnect from the add-in" is the remediation, not "retry".
|
|
643
|
+
export function capabilityUnknownError(backendURL, probe) {
|
|
644
|
+
const cause = probe?.error;
|
|
645
|
+
if (isSessionInvalidError(cause)) return cause;
|
|
646
|
+
if (cause?.code === "cli_update_required") return cause;
|
|
647
|
+
const attempts = probe?.attempts ?? 1;
|
|
648
|
+
const err = new Error(
|
|
649
|
+
`this slidemind add-in's capabilities could not be read` +
|
|
650
|
+
`${envPhrase(deriveEnvLabel(backendURL), "on")}` +
|
|
651
|
+
`${cause?.message ? ` (${cause.message})` : ""}` +
|
|
652
|
+
`${attempts > 1 ? ` after ${attempts} attempts` : ""}. ` +
|
|
653
|
+
"Nothing was decided from it: a capability that could not be discovered is neither present nor " +
|
|
654
|
+
"absent. Retry once the backend answers.",
|
|
655
|
+
);
|
|
656
|
+
err.code = "capability_unknown";
|
|
657
|
+
err.retryable = true;
|
|
658
|
+
err.cause = cause ?? null;
|
|
659
|
+
err.attempts = attempts;
|
|
660
|
+
if (cause?.status) err.status = cause.status;
|
|
661
|
+
if (cause?.headers) err.headers = cause.headers;
|
|
662
|
+
const retryAfter = retryAfterDelayMs(cause);
|
|
663
|
+
if (retryAfter !== null) err.retryAfterMs = retryAfter;
|
|
664
|
+
return err;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// The shared-record protocol a backend speaks (contracts.md §6), as a verdict
|
|
668
|
+
// the sync queue can act on without re-deriving it:
|
|
669
|
+
// supported `sharedRecords:{protocolVersion:1,kinds:[…]}` names `kind`, or
|
|
670
|
+
// is `["*"]` (any kind: the name is the client's namespace);
|
|
671
|
+
// no `kind` yet — a destination without a record — any kind will do
|
|
672
|
+
// unsupported the backend answered and does not advertise it (older add-in:
|
|
673
|
+
// the probe 404s; or it lists other kinds)
|
|
674
|
+
// unknown the probe FAILED for another reason (revoked session, relay
|
|
675
|
+
// down) — keep local data, block automated remote mutation;
|
|
676
|
+
// failure to discover permissions is not permission to write.
|
|
677
|
+
// `sharepointPublish` (deployment write policy) rides along as a separate
|
|
678
|
+
// condition; it is never folded into `status`.
|
|
679
|
+
export async function sharedRecordsSupport(
|
|
680
|
+
backendURL,
|
|
681
|
+
{ kind = null, protocolVersion = 1, refresh = false } = {},
|
|
682
|
+
) {
|
|
683
|
+
const { status: probe, caps, error } = await backendCapabilitiesResult(backendURL, { refresh });
|
|
684
|
+
const sr = caps?.sharedRecords;
|
|
685
|
+
const advertised =
|
|
686
|
+
sr && typeof sr === "object" && sr.protocolVersion === protocolVersion &&
|
|
687
|
+
Array.isArray(sr.kinds) && (sr.kinds.includes("*") || (kind ? sr.kinds.includes(kind) : sr.kinds.length > 0));
|
|
688
|
+
let status = "unsupported";
|
|
689
|
+
if (advertised) status = "supported";
|
|
690
|
+
else if (probe === "unknown") status = "unknown";
|
|
691
|
+
return {
|
|
692
|
+
status,
|
|
693
|
+
code: status === "unknown" ? "capability_unknown" : null,
|
|
694
|
+
sharedRecords: sr && typeof sr === "object" ? sr : null,
|
|
695
|
+
sharepointPublish: caps?.sharepointPublish === true,
|
|
696
|
+
error,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// The capabilities a backend advertises — `{}` for an add-in that predates the
|
|
701
|
+
// route — or a thrown `capability_unknown` when the probe got no answer. Every
|
|
702
|
+
// `backendSupports*` reader and every raw `caps.<flag>` check goes through
|
|
703
|
+
// here, so a probe failure can never be read as `false`: the one place an
|
|
704
|
+
// absent flag and a failed request used to look the same.
|
|
551
705
|
export async function backendCapabilities(backendURL) {
|
|
552
|
-
|
|
706
|
+
const probe = await backendCapabilitiesResult(backendURL);
|
|
707
|
+
if (probe.status === "unknown") throw capabilityUnknownError(backendURL, probe);
|
|
708
|
+
return probe.caps;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// The edit-safeguard contract (openspec `safeguard-slide-editing`, task 8.3)
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
//
|
|
715
|
+
// Every OTHER capability on this page is an OPTIONAL feature: absent, the CLI
|
|
716
|
+
// falls back (the pane relay instead of the pane-free engine routes) and keeps
|
|
717
|
+
// working. The edit safeguards are the opposite kind of thing. They are what
|
|
718
|
+
// stops an edit from silently dropping the element it was not asked to touch,
|
|
719
|
+
// so when an operation depends on one, an add-in that does not enforce it is a
|
|
720
|
+
// REFUSAL, never a fallback. The rule the whole change turns on applies here
|
|
721
|
+
// too: `unknown` is not `passed`, and a check that is absent is not a check
|
|
722
|
+
// that was satisfied.
|
|
723
|
+
//
|
|
724
|
+
// Two numbers are negotiated, and they are the ones the records already carry —
|
|
725
|
+
// no fourth version is minted here:
|
|
726
|
+
// `records` EDIT_OPERATION_VERSION / SLIDE_PRESERVATION_VERSION (one line:
|
|
727
|
+
// the operation and preservation sidecars are written together)
|
|
728
|
+
// `verdict` PRESERVATION_VERDICT_VERSION
|
|
729
|
+
// Both must EQUAL this CLI's. A backend behind us cannot honour a record it
|
|
730
|
+
// does not parse; a backend ahead of us may have changed what a field means,
|
|
731
|
+
// and guessing compatibility in that direction is how a safeguard becomes
|
|
732
|
+
// decorative. Neither direction is a version-FLOOR matter: `minCliVersion` is a
|
|
733
|
+
// hard bar for every command and must never exceed what npm has published,
|
|
734
|
+
// while this refuses exactly the operations that need the contract.
|
|
735
|
+
export const EDIT_SAFEGUARD_CAP_KEY = "editSafeguards";
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* The named safeguards a command can require. One vocabulary, shared with the
|
|
739
|
+
* add-in's advertisement — an operation asks for what it depends on, and the
|
|
740
|
+
* backend lists what it actually enforces.
|
|
741
|
+
*
|
|
742
|
+
* preservation-verdict the verdict is enforced at final delivery, so a
|
|
743
|
+
* render that lost a required element is refused
|
|
744
|
+
* operation-scope the backend honours the operation record's scope
|
|
745
|
+
* (targeted edit ≠ licence to rewrite the slide)
|
|
746
|
+
* bounded-repair repair runs inside the declared budget, or not at all
|
|
747
|
+
*/
|
|
748
|
+
export const EDIT_SAFEGUARDS = Object.freeze([
|
|
749
|
+
"preservation-verdict",
|
|
750
|
+
"operation-scope",
|
|
751
|
+
"bounded-repair",
|
|
752
|
+
]);
|
|
753
|
+
|
|
754
|
+
function contractVersions() {
|
|
755
|
+
// Imported lazily: transport.mjs is on the hot path of every command, and the
|
|
756
|
+
// record contracts pull in the whole validator graph.
|
|
757
|
+
return import("../../lib/edit-operations.mjs").then(async (ops) => ({
|
|
758
|
+
records: ops.EDIT_OPERATION_VERSION,
|
|
759
|
+
verdict: (await import("../../lib/preservation-verdict.mjs")).PRESERVATION_VERDICT_VERSION,
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const versionOf = (value) => (Number.isInteger(value) ? value : null);
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Does `backendURL` enforce the safeguards `need` names?
|
|
767
|
+
*
|
|
768
|
+
* Four outcomes, three statuses — the precision lives in `code` so the shape
|
|
769
|
+
* matches {@link sharedRecordsSupport}, the other versioned capability:
|
|
770
|
+
*
|
|
771
|
+
* supported it advertises our contract and enforces all of `need`
|
|
772
|
+
* unsupported/`…unsupported` it answered, and does not (older add-in, or a
|
|
773
|
+
* deployment that enforces only some of them)
|
|
774
|
+
* unsupported/`…contract_ahead` it speaks a NEWER contract than this CLI
|
|
775
|
+
* unsupported/`…contract_behind` it speaks an OLDER one
|
|
776
|
+
* unknown/`…unknown` the probe failed for any reason but a 404 —
|
|
777
|
+
* a revoked session, a relay that is down.
|
|
778
|
+
* Discovering nothing is not discovering that
|
|
779
|
+
* the check is unnecessary.
|
|
780
|
+
*
|
|
781
|
+
* `need` defaults to EVERY safeguard: a caller that forgets to say what it
|
|
782
|
+
* depends on gets the strictest answer, not a free pass. A name this CLI does
|
|
783
|
+
* not know is refused for the same reason — the backend cannot be advertising
|
|
784
|
+
* something neither side has a definition of.
|
|
785
|
+
*/
|
|
786
|
+
export async function editSafeguardSupport(
|
|
787
|
+
backendURL,
|
|
788
|
+
{ need = EDIT_SAFEGUARDS, refresh = false } = {},
|
|
789
|
+
) {
|
|
790
|
+
const wanted = (Array.isArray(need) ? need : [need]).filter(Boolean);
|
|
791
|
+
const ours = await contractVersions();
|
|
792
|
+
const base = { contract: null, ours, enforces: [], missing: wanted, error: null };
|
|
793
|
+
|
|
794
|
+
const unknownNames = wanted.filter((n) => !EDIT_SAFEGUARDS.includes(n));
|
|
795
|
+
if (unknownNames.length > 0) {
|
|
796
|
+
return { ...base, status: "unsupported", code: "safeguards_unsupported", missing: unknownNames };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const { status: probe, caps, error } = await backendCapabilitiesResult(backendURL, { refresh });
|
|
800
|
+
if (probe === "unknown") {
|
|
801
|
+
return { ...base, status: "unknown", code: "safeguards_unknown", error };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const advertised = caps?.[EDIT_SAFEGUARD_CAP_KEY];
|
|
805
|
+
if (!advertised || typeof advertised !== "object") {
|
|
806
|
+
return { ...base, status: "unsupported", code: "safeguards_unsupported", error };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const contract = {
|
|
810
|
+
records: versionOf(advertised.records),
|
|
811
|
+
verdict: versionOf(advertised.verdict),
|
|
812
|
+
};
|
|
813
|
+
// A malformed advertisement is an add-in that does not speak this contract,
|
|
814
|
+
// not one to be met halfway.
|
|
815
|
+
if (contract.records === null || contract.verdict === null) {
|
|
816
|
+
return { ...base, status: "unsupported", code: "safeguards_unsupported", contract, error };
|
|
817
|
+
}
|
|
818
|
+
if (contract.records > ours.records || contract.verdict > ours.verdict) {
|
|
819
|
+
return { ...base, status: "unsupported", code: "safeguards_contract_ahead", contract, error };
|
|
820
|
+
}
|
|
821
|
+
if (contract.records < ours.records || contract.verdict < ours.verdict) {
|
|
822
|
+
return { ...base, status: "unsupported", code: "safeguards_contract_behind", contract, error };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const enforces = Array.isArray(advertised.enforces)
|
|
826
|
+
? advertised.enforces.filter((n) => typeof n === "string")
|
|
827
|
+
: [];
|
|
828
|
+
const missing = wanted.filter((n) => !enforces.includes(n));
|
|
829
|
+
if (missing.length > 0) {
|
|
830
|
+
return { ...base, status: "unsupported", code: "safeguards_unsupported", contract, enforces, missing, error };
|
|
831
|
+
}
|
|
832
|
+
return { ...base, status: "supported", code: null, contract, enforces, missing: [], error };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function safeguardError(verdict, { need }) {
|
|
836
|
+
const names = need.join(", ");
|
|
837
|
+
const ours = `records=${verdict.ours.records} verdict=${verdict.ours.verdict}`;
|
|
838
|
+
const theirs = verdict.contract
|
|
839
|
+
? `records=${verdict.contract.records} verdict=${verdict.contract.verdict}`
|
|
840
|
+
: "no contract at all";
|
|
841
|
+
const nothing = "Nothing was written.";
|
|
842
|
+
let message;
|
|
843
|
+
switch (verdict.code) {
|
|
844
|
+
case "safeguards_unknown":
|
|
845
|
+
message =
|
|
846
|
+
`this add-in's capabilities could not be read` +
|
|
847
|
+
`${verdict.error?.message ? ` (${verdict.error.message})` : ""}, so whether it enforces ` +
|
|
848
|
+
`${names} is unknown. ${nothing} A check that could not be discovered is not a check ` +
|
|
849
|
+
`that passed — retry once the backend answers.`;
|
|
850
|
+
break;
|
|
851
|
+
case "safeguards_contract_ahead":
|
|
852
|
+
message =
|
|
853
|
+
`this add-in speaks a NEWER edit-safeguard contract (${theirs}) than this CLI (${ours}), ` +
|
|
854
|
+
`so ${names} cannot be requested safely. ${nothing} ${updateCliInstruction()}`;
|
|
855
|
+
break;
|
|
856
|
+
case "safeguards_contract_behind":
|
|
857
|
+
message =
|
|
858
|
+
`this add-in speaks an OLDER edit-safeguard contract (${theirs}) than this CLI (${ours}), ` +
|
|
859
|
+
`so it cannot honour ${names}. ${nothing} Wait for the deployment to update the slidemind ` +
|
|
860
|
+
`add-in — the safeguard is not something to work around.`;
|
|
861
|
+
break;
|
|
862
|
+
default:
|
|
863
|
+
message =
|
|
864
|
+
`this slidemind add-in does not enforce ${verdict.missing.join(", ") || names}, which this ` +
|
|
865
|
+
`operation depends on. ${nothing} Update the slidemind add-in to a build that advertises ` +
|
|
866
|
+
`\`${EDIT_SAFEGUARD_CAP_KEY}\`, or run an operation that does not require it.`;
|
|
867
|
+
}
|
|
868
|
+
const err = new Error(message);
|
|
869
|
+
err.code = verdict.code ?? "safeguards_unsupported";
|
|
870
|
+
err.safeguards = { need, missing: verdict.missing, contract: verdict.contract, ours: verdict.ours };
|
|
871
|
+
return err;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Refuse unless `backendURL` enforces every safeguard in `need`.
|
|
876
|
+
*
|
|
877
|
+
* `refresh` defaults to TRUE: this gate runs immediately before an operation
|
|
878
|
+
* that depends on the answer, and the 60s capability cache may predate a
|
|
879
|
+
* deployment that rolled back. The same rule the remote-mutation path follows —
|
|
880
|
+
* a write re-probes, a batch read may take the cached answer.
|
|
881
|
+
*/
|
|
882
|
+
export async function assertEditSafeguards(
|
|
883
|
+
backendURL,
|
|
884
|
+
{ need = EDIT_SAFEGUARDS, refresh = true } = {},
|
|
885
|
+
) {
|
|
886
|
+
const wanted = (Array.isArray(need) ? need : [need]).filter(Boolean);
|
|
887
|
+
const verdict = await editSafeguardSupport(backendURL, { need: wanted, refresh });
|
|
888
|
+
if (verdict.status === "supported") return verdict;
|
|
889
|
+
throw safeguardError(verdict, { need: wanted });
|
|
553
890
|
}
|
|
554
891
|
|
|
555
892
|
export async function backendSupportsRenderJobs(backendURL) {
|
|
@@ -571,11 +908,119 @@ export async function backendSupportsBrandRoutes(backendURL) {
|
|
|
571
908
|
// pane-free `/api/cli/brands/:brandId/concept-images` route (see
|
|
572
909
|
// generateConceptImagesForBrand). Advertised by newer add-ins only; older ones
|
|
573
910
|
// omit the flag and the caller degrades to "continue without".
|
|
911
|
+
// Whether a backend exports the native line directives (`data-connector` with
|
|
912
|
+
// `data-prst`/sites, `data-sm-line`, `data-sm-line-group` — the nativeLinesV1
|
|
913
|
+
// contract). The CLI writes ONLY that vocabulary, so an add-in that answered
|
|
914
|
+
// without the flag is a refusal, never a fallback to older markup.
|
|
915
|
+
// supported / unsupported (answered, no flag) / unknown (probe failed, not a 404)
|
|
916
|
+
export async function nativeLinesSupport(backendURL) {
|
|
917
|
+
if (!backendURL) return "unknown";
|
|
918
|
+
const { status, caps } = await backendCapabilitiesResult(backendURL, { refresh: true });
|
|
919
|
+
if (caps?.nativeLinesV1 === true) return "supported";
|
|
920
|
+
return status === "unknown" ? "unknown" : "unsupported";
|
|
921
|
+
}
|
|
922
|
+
|
|
574
923
|
export async function backendSupportsConceptImages(backendURL) {
|
|
575
924
|
const caps = await backendCapabilities(backendURL);
|
|
576
925
|
return caps?.conceptImages === true;
|
|
577
926
|
}
|
|
578
927
|
|
|
928
|
+
// Whether a backend extracts concept specs (a vision inventory of a mockup)
|
|
929
|
+
// over `/api/cli/brands/:brandId/concept-specs`. Older add-ins omit the flag
|
|
930
|
+
// and `slidemind concepts` writes PNGs only.
|
|
931
|
+
export async function backendSupportsConceptSpecs(backendURL) {
|
|
932
|
+
const caps = await backendCapabilities(backendURL);
|
|
933
|
+
return caps?.conceptSpecs === true;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// The same question as a verdict a command can print, naming WHICH add-in
|
|
937
|
+
// answered: a warning that only says "this add-in" leaves a user with several
|
|
938
|
+
// paired environments guessing which one lags (the 2026-10-06 proposal sim ran
|
|
939
|
+
// against prod while dev already read specs).
|
|
940
|
+
// supported / unsupported (answered without the flag) / unknown (no backend
|
|
941
|
+
// to ask, or the probe failed for a reason other than a 404)
|
|
942
|
+
export async function conceptSpecSupport(backendURL) {
|
|
943
|
+
if (!backendURL) return { status: "unknown", env: null, backendURL: null };
|
|
944
|
+
const probe = await backendCapabilitiesResult(backendURL);
|
|
945
|
+
let status = "unsupported";
|
|
946
|
+
if (probe.caps?.conceptSpecs === true) status = "supported";
|
|
947
|
+
else if (probe.status === "unknown") status = "unknown";
|
|
948
|
+
return { status, env: deriveEnvLabel(backendURL), backendURL };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Whether a backend runs concept-spec extraction as a polled job
|
|
952
|
+
// (`/api/cli/brands/:brandId/concept-spec-jobs`): no ingress timeout, an
|
|
953
|
+
// 8-minute per-slide budget server-side. Older add-ins answer the sync route.
|
|
954
|
+
export async function backendSupportsConceptSpecJobs(backendURL) {
|
|
955
|
+
const caps = await backendCapabilities(backendURL);
|
|
956
|
+
return caps?.conceptSpecJobs === true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// The image-intent contract (openspec `preserve-image-intent-across-generation`)
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
//
|
|
963
|
+
// `imageIntentV1` says the add-in understands the `imageIntent` envelope on the
|
|
964
|
+
// concept, spec, render and translate routes AND returns an
|
|
965
|
+
// `imageIntentValidation` verdict for it. Like the edit safeguards next door,
|
|
966
|
+
// and unlike every optional capability above it, this is a REFUSAL rather than
|
|
967
|
+
// a fallback: an add-in that ignores the envelope would generate and render a
|
|
968
|
+
// deck whose approved original never reached a slide, and report success.
|
|
969
|
+
//
|
|
970
|
+
// It is required only where an envelope actually travels. A deck whose plan
|
|
971
|
+
// records no image uses builds no envelope, so nothing about a legacy deck
|
|
972
|
+
// changes — no probe, no gate, the same request bytes as before.
|
|
973
|
+
export const IMAGE_INTENT_CAP_KEY = "imageIntentV1";
|
|
974
|
+
|
|
975
|
+
const IMAGE_INTENT_NOTHING =
|
|
976
|
+
"Nothing was generated and nothing was written — the deck's plan, its registry and its imported " +
|
|
977
|
+
"sources are exactly as they were.";
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Does `backendURL` speak the image-intent contract?
|
|
981
|
+
*
|
|
982
|
+
* supported it advertises `imageIntentV1`
|
|
983
|
+
* unsupported it answered, and does not (an older add-in deployment)
|
|
984
|
+
* unknown the probe failed for any reason but a 404 — discovering
|
|
985
|
+
* nothing is not discovering that the check is unnecessary
|
|
986
|
+
*/
|
|
987
|
+
export async function imageIntentSupport(backendURL, { refresh = false } = {}) {
|
|
988
|
+
const { status, caps, error } = await backendCapabilitiesResult(backendURL, { refresh });
|
|
989
|
+
if (caps?.[IMAGE_INTENT_CAP_KEY] === true) {
|
|
990
|
+
return { status: "supported", code: null, error: null };
|
|
991
|
+
}
|
|
992
|
+
if (status === "unknown") {
|
|
993
|
+
return { status: "unknown", code: "image_intent_unsupported", error };
|
|
994
|
+
}
|
|
995
|
+
return { status: "unsupported", code: "image_intent_unsupported", error };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Refuse, before the request, unless `backendURL` speaks the contract.
|
|
1000
|
+
*
|
|
1001
|
+
* `refresh` defaults to TRUE for the same reason the safeguard gate re-probes:
|
|
1002
|
+
* this runs immediately before the one operation that depends on the answer,
|
|
1003
|
+
* and the 60s capability cache may predate a rolled-back deployment.
|
|
1004
|
+
*/
|
|
1005
|
+
export async function assertImageIntentSupported(backendURL, { refresh = true } = {}) {
|
|
1006
|
+
const verdict = await imageIntentSupport(backendURL, { refresh });
|
|
1007
|
+
if (verdict.status === "supported") return verdict;
|
|
1008
|
+
const err = new Error(
|
|
1009
|
+
verdict.status === "unknown"
|
|
1010
|
+
? `this slidemind add-in's capabilities could not be read` +
|
|
1011
|
+
`${verdict.error?.message ? ` (${verdict.error.message})` : ""}, so whether it preserves this ` +
|
|
1012
|
+
`deck's approved image uses is unknown. ${IMAGE_INTENT_NOTHING} A capability that could not be ` +
|
|
1013
|
+
`discovered is not one that is present — retry once the backend answers.`
|
|
1014
|
+
: `this slidemind add-in does not support image intent (\`${IMAGE_INTENT_CAP_KEY}\`), which this ` +
|
|
1015
|
+
`deck's approved image uses depend on: it would drop the envelope, place no original and report ` +
|
|
1016
|
+
`success. ${IMAGE_INTENT_NOTHING} Wait for the deployment to update the slidemind add-in, or ` +
|
|
1017
|
+
`work in a deck whose plan records no image uses — those are unaffected.`,
|
|
1018
|
+
);
|
|
1019
|
+
err.code = "image_intent_unsupported";
|
|
1020
|
+
err.imageIntent = { capability: IMAGE_INTENT_CAP_KEY, status: verdict.status };
|
|
1021
|
+
throw err;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
579
1024
|
// Whether a backend generates regular images over the pane-free
|
|
580
1025
|
// `/api/cli/brands/:brandId/images` route (see generateImageForBrand).
|
|
581
1026
|
// Advertised by newer add-ins only.
|
|
@@ -623,6 +1068,18 @@ export async function backendSupportsGraphDelegated(backendURL) {
|
|
|
623
1068
|
return caps?.graphDelegated === true;
|
|
624
1069
|
}
|
|
625
1070
|
|
|
1071
|
+
// Whether the deployment's POLICY allows the CLI to publish back to SharePoint
|
|
1072
|
+
// (`slidemind publish`). A policy flag and nothing more: it does NOT prove the
|
|
1073
|
+
// user's token carries AllSites.Write, that admin consent was granted, or that
|
|
1074
|
+
// this item is writable. Those three are found at REQUEST time — a
|
|
1075
|
+
// `consent_required` challenge, a 403 on the write itself — never inferred from
|
|
1076
|
+
// a capability literal. Gating the command on it only keeps a deployment that
|
|
1077
|
+
// has switched the feature off from being asked in the first place.
|
|
1078
|
+
export async function backendSupportsSharePointPublish(backendURL) {
|
|
1079
|
+
const caps = await backendCapabilities(backendURL);
|
|
1080
|
+
return caps?.sharepointPublish === true;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
626
1083
|
// The add-in's hard version floor: /api/cli/capabilities advertises the oldest
|
|
627
1084
|
// CLI it still speaks (`minCliVersion`). An older CLI must refuse to proceed —
|
|
628
1085
|
// mid-rollout skew otherwise surfaces as confusing route/contract errors deep
|
|
@@ -654,9 +1111,27 @@ export async function engineGraphToken(backendURL, { signIn = true } = {}) {
|
|
|
654
1111
|
if (!token && signIn && (await ensureSignedIn(backendURL))) {
|
|
655
1112
|
token = await getGraphToken(backendURL);
|
|
656
1113
|
}
|
|
1114
|
+
if (!token) assertTokenRefreshed(backendURL, "graph");
|
|
657
1115
|
return token;
|
|
658
1116
|
}
|
|
659
1117
|
|
|
1118
|
+
// A stored sign-in whose token could not be refreshed just now is an outage to
|
|
1119
|
+
// retry, not a missing sign-in: nothing is sent without the token, because the
|
|
1120
|
+
// backend would answer `login_required` and the agent would send the user to
|
|
1121
|
+
// `slidemind login` for nothing (graph-auth.tokenRefreshFailure).
|
|
1122
|
+
function assertTokenRefreshed(backendURL, resource) {
|
|
1123
|
+
const cause = tokenRefreshFailure(backendURL, resource);
|
|
1124
|
+
if (!cause) return;
|
|
1125
|
+
const err = new Error(
|
|
1126
|
+
`the Microsoft 365 sign-in${envPhrase(deriveEnvLabel(backendURL), "for")} is intact, but its ` +
|
|
1127
|
+
`${resource === "sharepoint" ? "SharePoint" : "Graph"} token could not be refreshed just now ` +
|
|
1128
|
+
`(${cause}). Nothing was sent; retry the command.`,
|
|
1129
|
+
);
|
|
1130
|
+
err.code = "token_refresh_failed";
|
|
1131
|
+
err.retryable = true;
|
|
1132
|
+
throw err;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
660
1135
|
// The SharePoint-resource sibling (same sign-in, same refresh token): the
|
|
661
1136
|
// backend resolves `sg:` site-group membership from the PAIR, and on a
|
|
662
1137
|
// SharePoint-configured deployment refuses corpus search without it.
|
|
@@ -667,6 +1142,7 @@ export async function engineSpToken(backendURL, { signIn = true } = {}) {
|
|
|
667
1142
|
if (!token && signIn && (await ensureSignedIn(backendURL))) {
|
|
668
1143
|
token = await getSharePointToken(backendURL);
|
|
669
1144
|
}
|
|
1145
|
+
if (!token) assertTokenRefreshed(backendURL, "sharepoint");
|
|
670
1146
|
return token;
|
|
671
1147
|
}
|
|
672
1148
|
|
package/src/bridge/client.mjs
CHANGED
|
@@ -15,6 +15,8 @@ export {
|
|
|
15
15
|
getDesignBundleForBrandAnyBackend,
|
|
16
16
|
listBrands,
|
|
17
17
|
generateConceptImagesForBrand,
|
|
18
|
+
extractConceptSpecsForBrand,
|
|
19
|
+
fitCopyForBrand,
|
|
18
20
|
resolveConceptBackend,
|
|
19
21
|
generateImageForBrand,
|
|
20
22
|
backendSupportsConvertPptx,
|
|
@@ -33,6 +35,7 @@ export {
|
|
|
33
35
|
engineSearchAnyPane,
|
|
34
36
|
engineDownload,
|
|
35
37
|
engineDownloadToFile,
|
|
38
|
+
pickAssets,
|
|
36
39
|
} from "./client/engine-search.mjs";
|
|
37
40
|
export {
|
|
38
41
|
extractSlideToFile,
|
|
@@ -79,10 +82,17 @@ export {
|
|
|
79
82
|
isSessionInvalidError,
|
|
80
83
|
backendSupportsBrandRoutes,
|
|
81
84
|
backendSupportsConceptImages,
|
|
85
|
+
conceptSpecSupport,
|
|
86
|
+
nativeLinesSupport,
|
|
82
87
|
backendSupportsImageGeneration,
|
|
83
88
|
backendSupportsBrandsList,
|
|
84
89
|
backendSupportsSourcePresentation,
|
|
85
90
|
backendSupportsEngineRoutes,
|
|
86
91
|
backendSupportsGraphDelegated,
|
|
92
|
+
backendSupportsSharePointPublish,
|
|
87
93
|
assertCliVersionSupported,
|
|
94
|
+
EDIT_SAFEGUARDS,
|
|
95
|
+
EDIT_SAFEGUARD_CAP_KEY,
|
|
96
|
+
editSafeguardSupport,
|
|
97
|
+
assertEditSafeguards,
|
|
88
98
|
} from "./client/transport.mjs";
|