@nodegx/mcp 0.2.2 → 0.2.4
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 +10 -0
- package/dist/backend/cli.js +356 -245
- package/dist/cloud-bundle.cjs +898 -18
- package/dist/kit-extract.cjs +382 -222
- package/dist/kit-extract.inputs.json +1 -0
- package/dist/node-catalog.json +45 -1
- package/dist/noodl-mcp.cjs +3832 -630
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +45 -1
- package/dist/render-harness/data/node-catalog.json +45 -1
- package/dist/render-harness/drive-steps.js +95 -0
- package/dist/render-harness/measure-from-disk.js +95 -0
- package/dist/render-harness/render-from-disk.js +20 -1
- package/dist/src/external/viewer/global.d.ts.keep +13 -0
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +4 -6
package/dist/kit-extract.cjs
CHANGED
|
@@ -19679,6 +19679,35 @@ var require_simplejavascript = __commonJS({
|
|
|
19679
19679
|
}
|
|
19680
19680
|
return users;
|
|
19681
19681
|
}
|
|
19682
|
+
function placingSignalPorts(node) {
|
|
19683
|
+
const saved = node.model.outputPorts;
|
|
19684
|
+
const params = node.model.parameters || {};
|
|
19685
|
+
const names = [];
|
|
19686
|
+
for (const key in saved) {
|
|
19687
|
+
if (saved[key] && saved[key].type === "signal") names.push(key);
|
|
19688
|
+
}
|
|
19689
|
+
const declare = (key) => {
|
|
19690
|
+
if (!(key in saved) && names.indexOf(key) === -1) names.push(key);
|
|
19691
|
+
};
|
|
19692
|
+
const listed = [];
|
|
19693
|
+
const scriptOutputs = params["scriptOutputs"];
|
|
19694
|
+
if (Array.isArray(scriptOutputs)) {
|
|
19695
|
+
scriptOutputs.forEach((p) => {
|
|
19696
|
+
listed.push("out-" + p.label);
|
|
19697
|
+
if (params["outtype-" + p.label] === "signal") declare("out-" + p.label);
|
|
19698
|
+
});
|
|
19699
|
+
}
|
|
19700
|
+
const script = params["functionScript"];
|
|
19701
|
+
if (typeof script === "string") {
|
|
19702
|
+
const cached = node._internal.codeSignals;
|
|
19703
|
+
const signals = cached && cached.script === script ? cached.names : functionScriptPortNames(script).signals;
|
|
19704
|
+
node._internal.codeSignals = { script, names: signals };
|
|
19705
|
+
signals.forEach((n) => {
|
|
19706
|
+
if (listed.indexOf("out-" + n) === -1) declare("out-" + n);
|
|
19707
|
+
});
|
|
19708
|
+
}
|
|
19709
|
+
return names;
|
|
19710
|
+
}
|
|
19682
19711
|
function publishHeldWrites(node) {
|
|
19683
19712
|
node._internal.heldWrites.forEach((prop) => {
|
|
19684
19713
|
node.registerOutputIfNeeded("out-" + prop);
|
|
@@ -19951,18 +19980,16 @@ var require_simplejavascript = __commonJS({
|
|
|
19951
19980
|
}
|
|
19952
19981
|
const inputs = this._internal.inputValues;
|
|
19953
19982
|
const outputs = this._internal.outputValuesProxy;
|
|
19954
|
-
|
|
19955
|
-
|
|
19956
|
-
|
|
19957
|
-
|
|
19958
|
-
|
|
19959
|
-
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
}
|
|
19965
|
-
}
|
|
19983
|
+
placingSignalPorts(this).forEach((key) => {
|
|
19984
|
+
const _sendSignal = () => {
|
|
19985
|
+
this._internal.outputWrites.add(key.substring("out-".length));
|
|
19986
|
+
publishHeldWrites(this);
|
|
19987
|
+
if (this.hasOutput(key)) this.sendSignalOnOutput(key);
|
|
19988
|
+
};
|
|
19989
|
+
const signalValue = _sendSignal;
|
|
19990
|
+
signalValue.send = _sendSignal;
|
|
19991
|
+
this._internal.outputValues[key.substring("out-".length)] = signalValue;
|
|
19992
|
+
});
|
|
19966
19993
|
const noodlAPI = JavascriptNodeParser4.createNoodlAPI(this.nodeScope.modelScope);
|
|
19967
19994
|
noodlAPI.Inputs = inputs;
|
|
19968
19995
|
noodlAPI.Outputs = outputs;
|
|
@@ -21375,6 +21402,198 @@ var init_resolveBackend = __esm({
|
|
|
21375
21402
|
}
|
|
21376
21403
|
});
|
|
21377
21404
|
|
|
21405
|
+
// ../noodl-runtime/src/api/backends/SessionStore.ts
|
|
21406
|
+
function ambientStorage() {
|
|
21407
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
21408
|
+
return localStorage;
|
|
21409
|
+
}
|
|
21410
|
+
function ambientBroadcaster() {
|
|
21411
|
+
if (typeof window === "undefined" || !window || typeof window.addEventListener !== "function") return void 0;
|
|
21412
|
+
return window;
|
|
21413
|
+
}
|
|
21414
|
+
function parseSessionKey(appId) {
|
|
21415
|
+
return "Parse/" + appId + "/currentUser";
|
|
21416
|
+
}
|
|
21417
|
+
function parseInstallationIdKey(appId) {
|
|
21418
|
+
return "Parse/" + appId + "/installationId";
|
|
21419
|
+
}
|
|
21420
|
+
function parseSessionStore(appId) {
|
|
21421
|
+
const cacheKey = String(appId);
|
|
21422
|
+
let store = _parseStores.get(cacheKey);
|
|
21423
|
+
if (store === void 0) {
|
|
21424
|
+
store = new SessionStore({ key: parseSessionKey(appId) });
|
|
21425
|
+
_parseStores.set(cacheKey, store);
|
|
21426
|
+
}
|
|
21427
|
+
return store;
|
|
21428
|
+
}
|
|
21429
|
+
var _tabCounter, SessionStore, _parseStores;
|
|
21430
|
+
var init_SessionStore = __esm({
|
|
21431
|
+
"../noodl-runtime/src/api/backends/SessionStore.ts"() {
|
|
21432
|
+
_tabCounter = 0;
|
|
21433
|
+
SessionStore = class {
|
|
21434
|
+
constructor(options) {
|
|
21435
|
+
this.key = options.key;
|
|
21436
|
+
this.lockKey = options.key + ".refresh-lock";
|
|
21437
|
+
this.injectedStorage = options.storage;
|
|
21438
|
+
this.injectedBroadcaster = options.broadcaster;
|
|
21439
|
+
this.lockTtlMs = options.lockTtlMs !== void 0 ? options.lockTtlMs : 1e4;
|
|
21440
|
+
this.now = options.now || Date.now;
|
|
21441
|
+
this.tabId = options.tabId || "tab-" + ++_tabCounter + "-" + Math.random().toString(36).slice(2);
|
|
21442
|
+
}
|
|
21443
|
+
/**
|
|
21444
|
+
* Resolved per access, not captured at construction.
|
|
21445
|
+
*
|
|
21446
|
+
* The code this replaced was a bare `localStorage[key]` evaluated on every
|
|
21447
|
+
* request, so a page (or a suite) that swapped the global took effect
|
|
21448
|
+
* immediately. Capturing it once would look identical until something did —
|
|
21449
|
+
* and something does: `parse-wire-adapter.test.ts` replaces
|
|
21450
|
+
* `globalThis.localStorage` between cases, and caching broke it. Found by
|
|
21451
|
+
* running the suite that was not this task's.
|
|
21452
|
+
*/
|
|
21453
|
+
get storage() {
|
|
21454
|
+
if (this.injectedStorage !== void 0) return this.injectedStorage || void 0;
|
|
21455
|
+
return ambientStorage();
|
|
21456
|
+
}
|
|
21457
|
+
get broadcaster() {
|
|
21458
|
+
if (this.injectedBroadcaster !== void 0) return this.injectedBroadcaster || void 0;
|
|
21459
|
+
return ambientBroadcaster();
|
|
21460
|
+
}
|
|
21461
|
+
/** False under SSR and in the cloud runtime. Nothing here throws when it is. */
|
|
21462
|
+
get available() {
|
|
21463
|
+
return this.storage !== void 0;
|
|
21464
|
+
}
|
|
21465
|
+
// ── The session ──────────────────────────────────────────────────────────
|
|
21466
|
+
/** The stored text, or `undefined`. */
|
|
21467
|
+
readRaw() {
|
|
21468
|
+
const storage = this.storage;
|
|
21469
|
+
if (!storage) return void 0;
|
|
21470
|
+
const value2 = storage[this.key];
|
|
21471
|
+
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
21472
|
+
}
|
|
21473
|
+
/**
|
|
21474
|
+
* The stored session, or `undefined` when there is none *or it will not
|
|
21475
|
+
* parse*.
|
|
21476
|
+
*
|
|
21477
|
+
* Unparseable is treated as absent rather than raised, which is what all four
|
|
21478
|
+
* of the replaced call sites did — a corrupt entry must not stop an app from
|
|
21479
|
+
* loading, and the user can always sign in again.
|
|
21480
|
+
*/
|
|
21481
|
+
read() {
|
|
21482
|
+
const raw = this.readRaw();
|
|
21483
|
+
if (raw === void 0) return void 0;
|
|
21484
|
+
try {
|
|
21485
|
+
return JSON.parse(raw);
|
|
21486
|
+
} catch (e) {
|
|
21487
|
+
return void 0;
|
|
21488
|
+
}
|
|
21489
|
+
}
|
|
21490
|
+
write(session) {
|
|
21491
|
+
const storage = this.storage;
|
|
21492
|
+
if (!storage) return;
|
|
21493
|
+
storage[this.key] = JSON.stringify(session);
|
|
21494
|
+
}
|
|
21495
|
+
clear() {
|
|
21496
|
+
const storage = this.storage;
|
|
21497
|
+
if (!storage) return;
|
|
21498
|
+
delete storage[this.key];
|
|
21499
|
+
}
|
|
21500
|
+
/**
|
|
21501
|
+
* Read and write any key in the same storage, with the same absent-storage
|
|
21502
|
+
* tolerance.
|
|
21503
|
+
*
|
|
21504
|
+
* Here for Parse's installation id, which lives beside the session under
|
|
21505
|
+
* `Parse/<appId>/installationId` and is the only other thing `userservice.ts`
|
|
21506
|
+
* kept in local storage. It is not session state, so it gets no method of its
|
|
21507
|
+
* own — but it does need the SSR guard, and duplicating that guard is how the
|
|
21508
|
+
* four copies above happened in the first place.
|
|
21509
|
+
*/
|
|
21510
|
+
readKey(key) {
|
|
21511
|
+
const storage = this.storage;
|
|
21512
|
+
if (!storage) return void 0;
|
|
21513
|
+
const value2 = storage[key];
|
|
21514
|
+
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
21515
|
+
}
|
|
21516
|
+
writeKey(key, value2) {
|
|
21517
|
+
const storage = this.storage;
|
|
21518
|
+
if (!storage) return;
|
|
21519
|
+
storage[key] = value2;
|
|
21520
|
+
}
|
|
21521
|
+
// ── Cross-tab ────────────────────────────────────────────────────────────
|
|
21522
|
+
/**
|
|
21523
|
+
* Hear about session changes made by **other** tabs.
|
|
21524
|
+
*
|
|
21525
|
+
* The `storage` event fires only in tabs that did not make the change, which
|
|
21526
|
+
* is exactly the semantics wanted and is why this is preferred to
|
|
21527
|
+
* `BroadcastChannel`: no echo to filter out, no availability question, and it
|
|
21528
|
+
* comes free with the storage already in use.
|
|
21529
|
+
*
|
|
21530
|
+
* Returns an unsubscribe function. Returns a no-op one when there is no
|
|
21531
|
+
* broadcaster, so callers never branch.
|
|
21532
|
+
*/
|
|
21533
|
+
onExternalChange(handler) {
|
|
21534
|
+
const broadcaster = this.broadcaster;
|
|
21535
|
+
if (!broadcaster) return () => {
|
|
21536
|
+
};
|
|
21537
|
+
const listener = (event) => {
|
|
21538
|
+
if (event && event.key !== null && event.key !== this.key) return;
|
|
21539
|
+
handler(this.read());
|
|
21540
|
+
};
|
|
21541
|
+
broadcaster.addEventListener("storage", listener);
|
|
21542
|
+
return () => broadcaster.removeEventListener("storage", listener);
|
|
21543
|
+
}
|
|
21544
|
+
/**
|
|
21545
|
+
* Try to become the tab that refreshes.
|
|
21546
|
+
*
|
|
21547
|
+
* Read, write, **read back**. `localStorage` writes are serialised per origin,
|
|
21548
|
+
* so in the racing case both tabs write and both read back and exactly one
|
|
21549
|
+
* sees its own id — and the loser discovers that *before issuing any request*,
|
|
21550
|
+
* because the whole sequence is synchronous with no `await` in it. That is
|
|
21551
|
+
* what keeps the cost of a lost race at one tab doing nothing rather than two
|
|
21552
|
+
* tabs spending the same rotating refresh token.
|
|
21553
|
+
*
|
|
21554
|
+
* It is not a mutex and BCN-006-LIFECYCLE-DESIGN §5 does not claim it is. The
|
|
21555
|
+
* residual risk is a same-millisecond interleaving across processes on an
|
|
21556
|
+
* implementation that does not serialise writes; the cost if it happens is one
|
|
21557
|
+
* wasted refresh, and the follower's wait-then-adopt behaviour means the user
|
|
21558
|
+
* still ends up with the session the winner wrote.
|
|
21559
|
+
*
|
|
21560
|
+
* **The lock expires.** A tab closed mid-refresh must not hold it forever —
|
|
21561
|
+
* that would be worse than having no lock at all.
|
|
21562
|
+
*/
|
|
21563
|
+
tryAcquireRefreshLock() {
|
|
21564
|
+
const storage = this.storage;
|
|
21565
|
+
if (!storage) return false;
|
|
21566
|
+
const now = this.now();
|
|
21567
|
+
const existing = this.readLock();
|
|
21568
|
+
if (existing && existing.owner !== this.tabId && existing.expiresAt > now) return false;
|
|
21569
|
+
storage[this.lockKey] = JSON.stringify({ owner: this.tabId, expiresAt: now + this.lockTtlMs });
|
|
21570
|
+
const confirmed = this.readLock();
|
|
21571
|
+
return confirmed !== void 0 && confirmed.owner === this.tabId;
|
|
21572
|
+
}
|
|
21573
|
+
/** Release only our own lock — never one another tab has since taken. */
|
|
21574
|
+
releaseRefreshLock() {
|
|
21575
|
+
const storage = this.storage;
|
|
21576
|
+
if (!storage) return;
|
|
21577
|
+
const existing = this.readLock();
|
|
21578
|
+
if (existing && existing.owner !== this.tabId) return;
|
|
21579
|
+
delete storage[this.lockKey];
|
|
21580
|
+
}
|
|
21581
|
+
readLock() {
|
|
21582
|
+
const raw = this.readKey(this.lockKey);
|
|
21583
|
+
if (raw === void 0) return void 0;
|
|
21584
|
+
try {
|
|
21585
|
+
const parsed = JSON.parse(raw);
|
|
21586
|
+
if (!parsed || typeof parsed.owner !== "string" || typeof parsed.expiresAt !== "number") return void 0;
|
|
21587
|
+
return parsed;
|
|
21588
|
+
} catch (e) {
|
|
21589
|
+
return void 0;
|
|
21590
|
+
}
|
|
21591
|
+
}
|
|
21592
|
+
};
|
|
21593
|
+
_parseStores = /* @__PURE__ */ new Map();
|
|
21594
|
+
}
|
|
21595
|
+
});
|
|
21596
|
+
|
|
21378
21597
|
// ../nodegx-backend-contract/src/realtime.ts
|
|
21379
21598
|
function nextReconnectDelay(attempt, timing = REALTIME_TIMING) {
|
|
21380
21599
|
const n = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
|
|
@@ -21535,6 +21754,7 @@ var DEFAULT_TIMING, DEFAULT_HOST, RealtimeSubscription;
|
|
|
21535
21754
|
var init_RealtimeSubscription = __esm({
|
|
21536
21755
|
"../noodl-runtime/src/api/backends/realtime/RealtimeSubscription.ts"() {
|
|
21537
21756
|
init_resolveBackend_pure();
|
|
21757
|
+
init_SessionStore();
|
|
21538
21758
|
init_realtime2();
|
|
21539
21759
|
DEFAULT_TIMING = REALTIME_TIMING;
|
|
21540
21760
|
DEFAULT_HOST = {};
|
|
@@ -21775,9 +21995,20 @@ var init_RealtimeSubscription = __esm({
|
|
|
21775
21995
|
* fallback.
|
|
21776
21996
|
*/
|
|
21777
21997
|
get token() {
|
|
21778
|
-
if (isParseWireType(this.handle.type)) return this.handle.sessionToken || "";
|
|
21998
|
+
if (isParseWireType(this.handle.type)) return this.handle.sessionToken || this.storedSession() || "";
|
|
21779
21999
|
return this.handle.sessionToken || this.handle.publicToken || "";
|
|
21780
22000
|
}
|
|
22001
|
+
/**
|
|
22002
|
+
* 🔴 P121-TWO005-LIVEANON. The signed-in user's session, where a Parse-wire app keeps it: the session store, keyed by
|
|
22003
|
+
* the app id in `publicToken` — the same place `ParseWireAdapter` reads it from for every request
|
|
22004
|
+
* (`readStoredCurrentUser`). Nothing sets `handle.sessionToken` on this wire, so reading the handle alone made every
|
|
22005
|
+
* subscription anonymous for a signed-in member, and a collection only members may read refused it ("denies find
|
|
22006
|
+
* to anonymous") on every retry. SBR-011's arm passed a session ON THE HANDLE, which a real app never has.
|
|
22007
|
+
*/
|
|
22008
|
+
storedSession() {
|
|
22009
|
+
const stored = parseSessionStore(this.handle.publicToken).read();
|
|
22010
|
+
return stored && typeof stored.sessionToken === "string" ? stored.sessionToken : "";
|
|
22011
|
+
}
|
|
21781
22012
|
_armDeadline(generation) {
|
|
21782
22013
|
this._clearTimer("_deadlineTimer");
|
|
21783
22014
|
this._deadlineTimer = this._setTimeout(() => {
|
|
@@ -22321,12 +22552,11 @@ var init_SseTransport = __esm({
|
|
|
22321
22552
|
helloEvent: "connected",
|
|
22322
22553
|
changeEvents: () => ["change"],
|
|
22323
22554
|
resyncEvent: "resync",
|
|
22324
|
-
subscribeRequest(base,
|
|
22555
|
+
subscribeRequest(base, _token, clientId, subscriptions) {
|
|
22325
22556
|
const body = subscriptions.map(
|
|
22326
22557
|
(subscription) => subscription.where === void 0 ? { collection: subscription.collection } : { collection: subscription.collection, filter: subscription.where }
|
|
22327
22558
|
);
|
|
22328
22559
|
const headers = { "content-type": "application/json" };
|
|
22329
|
-
if (token) headers["authorization"] = "Bearer " + token;
|
|
22330
22560
|
return {
|
|
22331
22561
|
url: base + "/realtime/subscriptions",
|
|
22332
22562
|
init: {
|
|
@@ -24109,8 +24339,8 @@ var init_nodegx = __esm({
|
|
|
24109
24339
|
// The header works. What it cannot do is make a file private on behalf of
|
|
24110
24340
|
// nobody, and an author reading "supported" would not expect that.
|
|
24111
24341
|
"files.private": degraded(
|
|
24112
|
-
"A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone.",
|
|
24113
|
-
"BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal"
|
|
24342
|
+
"A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone. Upload File's Readers names roles that may read it too, such as role:member; a file with Readers is never readable by everyone.",
|
|
24343
|
+
"BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal. P121-TWO001-MEMBERSPHOTO: X-NodeGX-File-Readers adds read-only role entries to the file's ACL; uploaded with the master key and role:crew, anonymous and a user without the role are 403 (files-http.test.ts)"
|
|
24114
24344
|
),
|
|
24115
24345
|
"files.progress": supported("XHR upload path reports progress"),
|
|
24116
24346
|
"auth.password": supported("nodegx-backend/src/server/users.ts"),
|
|
@@ -25844,198 +26074,6 @@ var init_AdapterEvents = __esm({
|
|
|
25844
26074
|
}
|
|
25845
26075
|
});
|
|
25846
26076
|
|
|
25847
|
-
// ../noodl-runtime/src/api/backends/SessionStore.ts
|
|
25848
|
-
function ambientStorage() {
|
|
25849
|
-
if (typeof localStorage === "undefined") return void 0;
|
|
25850
|
-
return localStorage;
|
|
25851
|
-
}
|
|
25852
|
-
function ambientBroadcaster() {
|
|
25853
|
-
if (typeof window === "undefined" || !window || typeof window.addEventListener !== "function") return void 0;
|
|
25854
|
-
return window;
|
|
25855
|
-
}
|
|
25856
|
-
function parseSessionKey(appId) {
|
|
25857
|
-
return "Parse/" + appId + "/currentUser";
|
|
25858
|
-
}
|
|
25859
|
-
function parseInstallationIdKey(appId) {
|
|
25860
|
-
return "Parse/" + appId + "/installationId";
|
|
25861
|
-
}
|
|
25862
|
-
function parseSessionStore(appId) {
|
|
25863
|
-
const cacheKey = String(appId);
|
|
25864
|
-
let store = _parseStores.get(cacheKey);
|
|
25865
|
-
if (store === void 0) {
|
|
25866
|
-
store = new SessionStore({ key: parseSessionKey(appId) });
|
|
25867
|
-
_parseStores.set(cacheKey, store);
|
|
25868
|
-
}
|
|
25869
|
-
return store;
|
|
25870
|
-
}
|
|
25871
|
-
var _tabCounter, SessionStore, _parseStores;
|
|
25872
|
-
var init_SessionStore = __esm({
|
|
25873
|
-
"../noodl-runtime/src/api/backends/SessionStore.ts"() {
|
|
25874
|
-
_tabCounter = 0;
|
|
25875
|
-
SessionStore = class {
|
|
25876
|
-
constructor(options) {
|
|
25877
|
-
this.key = options.key;
|
|
25878
|
-
this.lockKey = options.key + ".refresh-lock";
|
|
25879
|
-
this.injectedStorage = options.storage;
|
|
25880
|
-
this.injectedBroadcaster = options.broadcaster;
|
|
25881
|
-
this.lockTtlMs = options.lockTtlMs !== void 0 ? options.lockTtlMs : 1e4;
|
|
25882
|
-
this.now = options.now || Date.now;
|
|
25883
|
-
this.tabId = options.tabId || "tab-" + ++_tabCounter + "-" + Math.random().toString(36).slice(2);
|
|
25884
|
-
}
|
|
25885
|
-
/**
|
|
25886
|
-
* Resolved per access, not captured at construction.
|
|
25887
|
-
*
|
|
25888
|
-
* The code this replaced was a bare `localStorage[key]` evaluated on every
|
|
25889
|
-
* request, so a page (or a suite) that swapped the global took effect
|
|
25890
|
-
* immediately. Capturing it once would look identical until something did —
|
|
25891
|
-
* and something does: `parse-wire-adapter.test.ts` replaces
|
|
25892
|
-
* `globalThis.localStorage` between cases, and caching broke it. Found by
|
|
25893
|
-
* running the suite that was not this task's.
|
|
25894
|
-
*/
|
|
25895
|
-
get storage() {
|
|
25896
|
-
if (this.injectedStorage !== void 0) return this.injectedStorage || void 0;
|
|
25897
|
-
return ambientStorage();
|
|
25898
|
-
}
|
|
25899
|
-
get broadcaster() {
|
|
25900
|
-
if (this.injectedBroadcaster !== void 0) return this.injectedBroadcaster || void 0;
|
|
25901
|
-
return ambientBroadcaster();
|
|
25902
|
-
}
|
|
25903
|
-
/** False under SSR and in the cloud runtime. Nothing here throws when it is. */
|
|
25904
|
-
get available() {
|
|
25905
|
-
return this.storage !== void 0;
|
|
25906
|
-
}
|
|
25907
|
-
// ── The session ──────────────────────────────────────────────────────────
|
|
25908
|
-
/** The stored text, or `undefined`. */
|
|
25909
|
-
readRaw() {
|
|
25910
|
-
const storage = this.storage;
|
|
25911
|
-
if (!storage) return void 0;
|
|
25912
|
-
const value2 = storage[this.key];
|
|
25913
|
-
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
25914
|
-
}
|
|
25915
|
-
/**
|
|
25916
|
-
* The stored session, or `undefined` when there is none *or it will not
|
|
25917
|
-
* parse*.
|
|
25918
|
-
*
|
|
25919
|
-
* Unparseable is treated as absent rather than raised, which is what all four
|
|
25920
|
-
* of the replaced call sites did — a corrupt entry must not stop an app from
|
|
25921
|
-
* loading, and the user can always sign in again.
|
|
25922
|
-
*/
|
|
25923
|
-
read() {
|
|
25924
|
-
const raw = this.readRaw();
|
|
25925
|
-
if (raw === void 0) return void 0;
|
|
25926
|
-
try {
|
|
25927
|
-
return JSON.parse(raw);
|
|
25928
|
-
} catch (e) {
|
|
25929
|
-
return void 0;
|
|
25930
|
-
}
|
|
25931
|
-
}
|
|
25932
|
-
write(session) {
|
|
25933
|
-
const storage = this.storage;
|
|
25934
|
-
if (!storage) return;
|
|
25935
|
-
storage[this.key] = JSON.stringify(session);
|
|
25936
|
-
}
|
|
25937
|
-
clear() {
|
|
25938
|
-
const storage = this.storage;
|
|
25939
|
-
if (!storage) return;
|
|
25940
|
-
delete storage[this.key];
|
|
25941
|
-
}
|
|
25942
|
-
/**
|
|
25943
|
-
* Read and write any key in the same storage, with the same absent-storage
|
|
25944
|
-
* tolerance.
|
|
25945
|
-
*
|
|
25946
|
-
* Here for Parse's installation id, which lives beside the session under
|
|
25947
|
-
* `Parse/<appId>/installationId` and is the only other thing `userservice.ts`
|
|
25948
|
-
* kept in local storage. It is not session state, so it gets no method of its
|
|
25949
|
-
* own — but it does need the SSR guard, and duplicating that guard is how the
|
|
25950
|
-
* four copies above happened in the first place.
|
|
25951
|
-
*/
|
|
25952
|
-
readKey(key) {
|
|
25953
|
-
const storage = this.storage;
|
|
25954
|
-
if (!storage) return void 0;
|
|
25955
|
-
const value2 = storage[key];
|
|
25956
|
-
return value2 === void 0 || value2 === null ? void 0 : String(value2);
|
|
25957
|
-
}
|
|
25958
|
-
writeKey(key, value2) {
|
|
25959
|
-
const storage = this.storage;
|
|
25960
|
-
if (!storage) return;
|
|
25961
|
-
storage[key] = value2;
|
|
25962
|
-
}
|
|
25963
|
-
// ── Cross-tab ────────────────────────────────────────────────────────────
|
|
25964
|
-
/**
|
|
25965
|
-
* Hear about session changes made by **other** tabs.
|
|
25966
|
-
*
|
|
25967
|
-
* The `storage` event fires only in tabs that did not make the change, which
|
|
25968
|
-
* is exactly the semantics wanted and is why this is preferred to
|
|
25969
|
-
* `BroadcastChannel`: no echo to filter out, no availability question, and it
|
|
25970
|
-
* comes free with the storage already in use.
|
|
25971
|
-
*
|
|
25972
|
-
* Returns an unsubscribe function. Returns a no-op one when there is no
|
|
25973
|
-
* broadcaster, so callers never branch.
|
|
25974
|
-
*/
|
|
25975
|
-
onExternalChange(handler) {
|
|
25976
|
-
const broadcaster = this.broadcaster;
|
|
25977
|
-
if (!broadcaster) return () => {
|
|
25978
|
-
};
|
|
25979
|
-
const listener = (event) => {
|
|
25980
|
-
if (event && event.key !== null && event.key !== this.key) return;
|
|
25981
|
-
handler(this.read());
|
|
25982
|
-
};
|
|
25983
|
-
broadcaster.addEventListener("storage", listener);
|
|
25984
|
-
return () => broadcaster.removeEventListener("storage", listener);
|
|
25985
|
-
}
|
|
25986
|
-
/**
|
|
25987
|
-
* Try to become the tab that refreshes.
|
|
25988
|
-
*
|
|
25989
|
-
* Read, write, **read back**. `localStorage` writes are serialised per origin,
|
|
25990
|
-
* so in the racing case both tabs write and both read back and exactly one
|
|
25991
|
-
* sees its own id — and the loser discovers that *before issuing any request*,
|
|
25992
|
-
* because the whole sequence is synchronous with no `await` in it. That is
|
|
25993
|
-
* what keeps the cost of a lost race at one tab doing nothing rather than two
|
|
25994
|
-
* tabs spending the same rotating refresh token.
|
|
25995
|
-
*
|
|
25996
|
-
* It is not a mutex and BCN-006-LIFECYCLE-DESIGN §5 does not claim it is. The
|
|
25997
|
-
* residual risk is a same-millisecond interleaving across processes on an
|
|
25998
|
-
* implementation that does not serialise writes; the cost if it happens is one
|
|
25999
|
-
* wasted refresh, and the follower's wait-then-adopt behaviour means the user
|
|
26000
|
-
* still ends up with the session the winner wrote.
|
|
26001
|
-
*
|
|
26002
|
-
* **The lock expires.** A tab closed mid-refresh must not hold it forever —
|
|
26003
|
-
* that would be worse than having no lock at all.
|
|
26004
|
-
*/
|
|
26005
|
-
tryAcquireRefreshLock() {
|
|
26006
|
-
const storage = this.storage;
|
|
26007
|
-
if (!storage) return false;
|
|
26008
|
-
const now = this.now();
|
|
26009
|
-
const existing = this.readLock();
|
|
26010
|
-
if (existing && existing.owner !== this.tabId && existing.expiresAt > now) return false;
|
|
26011
|
-
storage[this.lockKey] = JSON.stringify({ owner: this.tabId, expiresAt: now + this.lockTtlMs });
|
|
26012
|
-
const confirmed = this.readLock();
|
|
26013
|
-
return confirmed !== void 0 && confirmed.owner === this.tabId;
|
|
26014
|
-
}
|
|
26015
|
-
/** Release only our own lock — never one another tab has since taken. */
|
|
26016
|
-
releaseRefreshLock() {
|
|
26017
|
-
const storage = this.storage;
|
|
26018
|
-
if (!storage) return;
|
|
26019
|
-
const existing = this.readLock();
|
|
26020
|
-
if (existing && existing.owner !== this.tabId) return;
|
|
26021
|
-
delete storage[this.lockKey];
|
|
26022
|
-
}
|
|
26023
|
-
readLock() {
|
|
26024
|
-
const raw = this.readKey(this.lockKey);
|
|
26025
|
-
if (raw === void 0) return void 0;
|
|
26026
|
-
try {
|
|
26027
|
-
const parsed = JSON.parse(raw);
|
|
26028
|
-
if (!parsed || typeof parsed.owner !== "string" || typeof parsed.expiresAt !== "number") return void 0;
|
|
26029
|
-
return parsed;
|
|
26030
|
-
} catch (e) {
|
|
26031
|
-
return void 0;
|
|
26032
|
-
}
|
|
26033
|
-
}
|
|
26034
|
-
};
|
|
26035
|
-
_parseStores = /* @__PURE__ */ new Map();
|
|
26036
|
-
}
|
|
26037
|
-
});
|
|
26038
|
-
|
|
26039
26077
|
// ../noodl-runtime/src/api/backends/fileRef.ts
|
|
26040
26078
|
function jwtExpiry(token) {
|
|
26041
26079
|
if (typeof token !== "string") return void 0;
|
|
@@ -26169,6 +26207,13 @@ function withStatus(err, status) {
|
|
|
26169
26207
|
if (status !== void 0 && status > 0) envelope.status = status;
|
|
26170
26208
|
return envelope;
|
|
26171
26209
|
}
|
|
26210
|
+
function uploadHeaders(options) {
|
|
26211
|
+
const headers = {};
|
|
26212
|
+
if (options.private) headers["X-NodeGX-File-Private"] = "true";
|
|
26213
|
+
const readers = typeof options.readers === "string" ? options.readers.trim() : "";
|
|
26214
|
+
if (readers) headers["X-NodeGX-File-Readers"] = readers;
|
|
26215
|
+
return Object.keys(headers).length ? headers : void 0;
|
|
26216
|
+
}
|
|
26172
26217
|
function _removeProtectedFields(data, className) {
|
|
26173
26218
|
const _data = Object.assign({}, data);
|
|
26174
26219
|
_protectedFields._common.forEach((f) => delete _data[f]);
|
|
@@ -26586,10 +26631,10 @@ var init_ParseWireAdapter = __esm({
|
|
|
26586
26631
|
content: options.file,
|
|
26587
26632
|
// Dead — `_makeRequest` never reads `contentType`. See the module note.
|
|
26588
26633
|
contentType: options.file.type,
|
|
26589
|
-
// BAK-006 follow-up: the Upload File node's Private input
|
|
26590
|
-
//
|
|
26591
|
-
// upload to its owner instead of leaving it public.
|
|
26592
|
-
headers: options
|
|
26634
|
+
// BAK-006 follow-up: the Upload File node's Private input, and (P121
|
|
26635
|
+
// MEMBERSPHOTO) its Readers. The backend reads these headers to ACL the
|
|
26636
|
+
// upload to its owner, and to those roles, instead of leaving it public.
|
|
26637
|
+
headers: uploadHeaders(options),
|
|
26593
26638
|
// BCN-007 step 1. This used to be
|
|
26594
26639
|
// `Object.assign({}, options.data, response)` cast to `{name, url}` —
|
|
26595
26640
|
// whatever the backend sent, spread over a caller-supplied object no
|
|
@@ -66748,9 +66793,9 @@ var init_navigation_stack = __esm({
|
|
|
66748
66793
|
}
|
|
66749
66794
|
});
|
|
66750
66795
|
|
|
66751
|
-
// type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/
|
|
66796
|
+
// type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/own020-pack024/packages/noodl-viewer-react/src/types.ts
|
|
66752
66797
|
var require_types2 = __commonJS({
|
|
66753
|
-
"type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/
|
|
66798
|
+
"type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/own020-pack024/packages/noodl-viewer-react/src/types.ts"(exports2, module2) {
|
|
66754
66799
|
var noop2 = new Proxy(function() {
|
|
66755
66800
|
}, { get: () => noop2, apply: () => noop2 });
|
|
66756
66801
|
module2.exports = new Proxy({}, { get: () => noop2 });
|
|
@@ -86381,6 +86426,76 @@ var init_openfilepicker = __esm({
|
|
|
86381
86426
|
}
|
|
86382
86427
|
});
|
|
86383
86428
|
|
|
86429
|
+
// ../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts
|
|
86430
|
+
function shrunkSize(width, height, largestSide) {
|
|
86431
|
+
const longer = Math.max(width, height);
|
|
86432
|
+
if (!(largestSide > 0) || !(longer > largestSide)) return void 0;
|
|
86433
|
+
const scale = largestSide / longer;
|
|
86434
|
+
return {
|
|
86435
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
86436
|
+
height: Math.max(1, Math.round(height * scale))
|
|
86437
|
+
};
|
|
86438
|
+
}
|
|
86439
|
+
function shrunkType(type) {
|
|
86440
|
+
return KEEPS_ITS_TYPE.has(type) ? type : "image/jpeg";
|
|
86441
|
+
}
|
|
86442
|
+
function isShrinkable(type) {
|
|
86443
|
+
return typeof type === "string" && type.startsWith("image/") && !LEFT_ALONE.has(type);
|
|
86444
|
+
}
|
|
86445
|
+
function shrunkName(name2, fromType, toType) {
|
|
86446
|
+
if (fromType === toType) return name2;
|
|
86447
|
+
const ext = toType === "image/jpeg" ? "jpg" : toType.slice("image/".length);
|
|
86448
|
+
const dot = name2.lastIndexOf(".");
|
|
86449
|
+
return `${dot > 0 ? name2.slice(0, dot) : name2 || "photo"}.${ext}`;
|
|
86450
|
+
}
|
|
86451
|
+
function qualityOf(quality) {
|
|
86452
|
+
const q = typeof quality === "number" && quality > 0 && quality <= 100 ? quality : DEFAULT_PHOTO_QUALITY;
|
|
86453
|
+
return q / 100;
|
|
86454
|
+
}
|
|
86455
|
+
async function shrinkPhoto(file, options, ops = browserPhotoOps) {
|
|
86456
|
+
if (!(options.largestSide > 0) || !isShrinkable(file.type)) return file;
|
|
86457
|
+
let photo;
|
|
86458
|
+
try {
|
|
86459
|
+
photo = await ops.decode(file);
|
|
86460
|
+
const size = shrunkSize(photo.width, photo.height, options.largestSide);
|
|
86461
|
+
if (!size) return file;
|
|
86462
|
+
const type = shrunkType(file.type);
|
|
86463
|
+
const blob = await ops.encode(photo, size.width, size.height, type, qualityOf(options.quality));
|
|
86464
|
+
if (!blob || blob.size === 0 || blob.size >= file.size) return file;
|
|
86465
|
+
return new File([blob], shrunkName(file.name, file.type, type), {
|
|
86466
|
+
type,
|
|
86467
|
+
lastModified: file.lastModified
|
|
86468
|
+
});
|
|
86469
|
+
} catch (_e) {
|
|
86470
|
+
return file;
|
|
86471
|
+
} finally {
|
|
86472
|
+
if (photo && photo.close) photo.close();
|
|
86473
|
+
}
|
|
86474
|
+
}
|
|
86475
|
+
var DEFAULT_PHOTO_QUALITY, KEEPS_ITS_TYPE, LEFT_ALONE, browserPhotoOps;
|
|
86476
|
+
var init_shrinkphoto = __esm({
|
|
86477
|
+
"../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts"() {
|
|
86478
|
+
DEFAULT_PHOTO_QUALITY = 80;
|
|
86479
|
+
KEEPS_ITS_TYPE = /* @__PURE__ */ new Set(["image/png", "image/webp", "image/jpeg"]);
|
|
86480
|
+
LEFT_ALONE = /* @__PURE__ */ new Set(["image/gif", "image/svg+xml"]);
|
|
86481
|
+
browserPhotoOps = {
|
|
86482
|
+
async decode(file) {
|
|
86483
|
+
return createImageBitmap(file, { imageOrientation: "from-image" });
|
|
86484
|
+
},
|
|
86485
|
+
encode(photo, width, height, type, quality) {
|
|
86486
|
+
const canvas = document.createElement("canvas");
|
|
86487
|
+
canvas.width = width;
|
|
86488
|
+
canvas.height = height;
|
|
86489
|
+
const ctx = canvas.getContext("2d");
|
|
86490
|
+
if (!ctx) return Promise.resolve(null);
|
|
86491
|
+
ctx.imageSmoothingQuality = "high";
|
|
86492
|
+
ctx.drawImage(photo, 0, 0, width, height);
|
|
86493
|
+
return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
86494
|
+
}
|
|
86495
|
+
};
|
|
86496
|
+
}
|
|
86497
|
+
});
|
|
86498
|
+
|
|
86384
86499
|
// ../noodl-viewer-react/src/nodes/std-library/uploadfile.ts
|
|
86385
86500
|
var uploadfile_exports = {};
|
|
86386
86501
|
__export(uploadfile_exports, {
|
|
@@ -86401,6 +86516,7 @@ var init_uploadfile = __esm({
|
|
|
86401
86516
|
init_schema_ports();
|
|
86402
86517
|
init_fileRef();
|
|
86403
86518
|
init_outcome();
|
|
86519
|
+
init_shrinkphoto();
|
|
86404
86520
|
UPLOAD_ERROR_CODE = "upload-file/upload-failed";
|
|
86405
86521
|
UploadFile = {
|
|
86406
86522
|
name: "Upload File",
|
|
@@ -86439,6 +86555,42 @@ var init_uploadfile = __esm({
|
|
|
86439
86555
|
this._internal.private = value2;
|
|
86440
86556
|
}
|
|
86441
86557
|
},
|
|
86558
|
+
// P121-TWO001-MEMBERSPHOTO: Private alone is the uploader's alone, and a feed shows every
|
|
86559
|
+
// member's photos to every other member. `X-NodeGX-File-Readers` shares a private file with
|
|
86560
|
+
// roles; the backend refuses a malformed list, or a role the uploader does not hold, in words.
|
|
86561
|
+
readers: {
|
|
86562
|
+
group: "General",
|
|
86563
|
+
displayName: "Readers",
|
|
86564
|
+
type: "string",
|
|
86565
|
+
description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only",
|
|
86566
|
+
set(value2) {
|
|
86567
|
+
this._internal.readers = value2;
|
|
86568
|
+
}
|
|
86569
|
+
},
|
|
86570
|
+
// ── P121-TWO008-UPLOADSHRINK: Shrink Photos ──────────────────────────
|
|
86571
|
+
//
|
|
86572
|
+
// A phone photo is 3 to 8 MB, and the backend's thumbnails shrink what is served, not what
|
|
86573
|
+
// is sent. Off unless set, so every existing graph sends exactly what it sent before.
|
|
86574
|
+
shrinkTo: {
|
|
86575
|
+
group: "Shrink Photos",
|
|
86576
|
+
displayName: "Shrink To",
|
|
86577
|
+
type: "number",
|
|
86578
|
+
default: 0,
|
|
86579
|
+
description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked",
|
|
86580
|
+
set(value2) {
|
|
86581
|
+
this._internal.shrinkTo = value2;
|
|
86582
|
+
}
|
|
86583
|
+
},
|
|
86584
|
+
photoQuality: {
|
|
86585
|
+
group: "Shrink Photos",
|
|
86586
|
+
displayName: "Photo Quality",
|
|
86587
|
+
type: "number",
|
|
86588
|
+
default: 80,
|
|
86589
|
+
description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk",
|
|
86590
|
+
set(value2) {
|
|
86591
|
+
this._internal.photoQuality = value2;
|
|
86592
|
+
}
|
|
86593
|
+
},
|
|
86442
86594
|
// ── BCN-007 step 3: File Location ────────────────────────────────────
|
|
86443
86595
|
//
|
|
86444
86596
|
// ⚠️ **Two of the five backends cannot store a file without being told
|
|
@@ -86516,10 +86668,11 @@ var init_uploadfile = __esm({
|
|
|
86516
86668
|
}
|
|
86517
86669
|
const store = this.cloudStore([outcome]);
|
|
86518
86670
|
if (!store) return;
|
|
86519
|
-
store.uploadFile({
|
|
86520
|
-
file,
|
|
86671
|
+
const send = (sent) => store.uploadFile({
|
|
86672
|
+
file: sent,
|
|
86521
86673
|
private: this._internal.private,
|
|
86522
|
-
|
|
86674
|
+
readers: this._internal.readers,
|
|
86675
|
+
target: this.fileTarget(sent),
|
|
86523
86676
|
onUploadProgress: (p) => {
|
|
86524
86677
|
this._internal.progressTotal = p.total;
|
|
86525
86678
|
this._internal.progressLoaded = p.loaded;
|
|
@@ -86535,14 +86688,20 @@ var init_uploadfile = __esm({
|
|
|
86535
86688
|
// structurally rather than imported, because `cloudstore.js` is still
|
|
86536
86689
|
// JavaScript and declares this callback `unknown` (PLAT-006 residual).
|
|
86537
86690
|
success: (response) => {
|
|
86538
|
-
const
|
|
86539
|
-
const filename = response.filename !== void 0 ? response.filename :
|
|
86691
|
+
const sentName = sent && typeof sent.name === "string" ? sent.name : void 0;
|
|
86692
|
+
const filename = response.filename !== void 0 ? response.filename : sentName;
|
|
86540
86693
|
this._internal.cloudFile = new import_cloudfile.default(filename !== void 0 ? Object.assign({}, response, { filename }) : response);
|
|
86541
86694
|
this.flagOutputDirty("cloudFile");
|
|
86542
86695
|
this.reportOutcome(outcome, "done");
|
|
86543
86696
|
},
|
|
86544
86697
|
error: (e) => this.setError(e, [outcome])
|
|
86545
86698
|
});
|
|
86699
|
+
const shrinkTo = this._internal.shrinkTo;
|
|
86700
|
+
if (typeof shrinkTo === "number" && shrinkTo > 0 && isShrinkable(file.type)) {
|
|
86701
|
+
shrinkPhoto(file, { largestSide: shrinkTo, quality: this._internal.photoQuality }).then(send);
|
|
86702
|
+
} else {
|
|
86703
|
+
send(file);
|
|
86704
|
+
}
|
|
86546
86705
|
});
|
|
86547
86706
|
}
|
|
86548
86707
|
}
|
|
@@ -86690,8 +86849,9 @@ var init_uploadfile = __esm({
|
|
|
86690
86849
|
* meaningful: a project has one backend selected, and only one of the two
|
|
86691
86850
|
* backends reads either group.
|
|
86692
86851
|
*/
|
|
86693
|
-
fileTarget() {
|
|
86694
|
-
const { bucket, path: path2, collection, recordId, field
|
|
86852
|
+
fileTarget(sent) {
|
|
86853
|
+
const { bucket, path: path2, collection, recordId, field } = this._internal;
|
|
86854
|
+
const file = sent || this._internal.file;
|
|
86695
86855
|
if (bucket) {
|
|
86696
86856
|
return { kind: "bucket", bucket, path: path2 || file?.name || "" };
|
|
86697
86857
|
}
|