@nodegx/mcp 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21375,6 +21375,198 @@ var init_resolveBackend = __esm({
21375
21375
  }
21376
21376
  });
21377
21377
 
21378
+ // ../noodl-runtime/src/api/backends/SessionStore.ts
21379
+ function ambientStorage() {
21380
+ if (typeof localStorage === "undefined") return void 0;
21381
+ return localStorage;
21382
+ }
21383
+ function ambientBroadcaster() {
21384
+ if (typeof window === "undefined" || !window || typeof window.addEventListener !== "function") return void 0;
21385
+ return window;
21386
+ }
21387
+ function parseSessionKey(appId) {
21388
+ return "Parse/" + appId + "/currentUser";
21389
+ }
21390
+ function parseInstallationIdKey(appId) {
21391
+ return "Parse/" + appId + "/installationId";
21392
+ }
21393
+ function parseSessionStore(appId) {
21394
+ const cacheKey = String(appId);
21395
+ let store = _parseStores.get(cacheKey);
21396
+ if (store === void 0) {
21397
+ store = new SessionStore({ key: parseSessionKey(appId) });
21398
+ _parseStores.set(cacheKey, store);
21399
+ }
21400
+ return store;
21401
+ }
21402
+ var _tabCounter, SessionStore, _parseStores;
21403
+ var init_SessionStore = __esm({
21404
+ "../noodl-runtime/src/api/backends/SessionStore.ts"() {
21405
+ _tabCounter = 0;
21406
+ SessionStore = class {
21407
+ constructor(options) {
21408
+ this.key = options.key;
21409
+ this.lockKey = options.key + ".refresh-lock";
21410
+ this.injectedStorage = options.storage;
21411
+ this.injectedBroadcaster = options.broadcaster;
21412
+ this.lockTtlMs = options.lockTtlMs !== void 0 ? options.lockTtlMs : 1e4;
21413
+ this.now = options.now || Date.now;
21414
+ this.tabId = options.tabId || "tab-" + ++_tabCounter + "-" + Math.random().toString(36).slice(2);
21415
+ }
21416
+ /**
21417
+ * Resolved per access, not captured at construction.
21418
+ *
21419
+ * The code this replaced was a bare `localStorage[key]` evaluated on every
21420
+ * request, so a page (or a suite) that swapped the global took effect
21421
+ * immediately. Capturing it once would look identical until something did —
21422
+ * and something does: `parse-wire-adapter.test.ts` replaces
21423
+ * `globalThis.localStorage` between cases, and caching broke it. Found by
21424
+ * running the suite that was not this task's.
21425
+ */
21426
+ get storage() {
21427
+ if (this.injectedStorage !== void 0) return this.injectedStorage || void 0;
21428
+ return ambientStorage();
21429
+ }
21430
+ get broadcaster() {
21431
+ if (this.injectedBroadcaster !== void 0) return this.injectedBroadcaster || void 0;
21432
+ return ambientBroadcaster();
21433
+ }
21434
+ /** False under SSR and in the cloud runtime. Nothing here throws when it is. */
21435
+ get available() {
21436
+ return this.storage !== void 0;
21437
+ }
21438
+ // ── The session ──────────────────────────────────────────────────────────
21439
+ /** The stored text, or `undefined`. */
21440
+ readRaw() {
21441
+ const storage = this.storage;
21442
+ if (!storage) return void 0;
21443
+ const value2 = storage[this.key];
21444
+ return value2 === void 0 || value2 === null ? void 0 : String(value2);
21445
+ }
21446
+ /**
21447
+ * The stored session, or `undefined` when there is none *or it will not
21448
+ * parse*.
21449
+ *
21450
+ * Unparseable is treated as absent rather than raised, which is what all four
21451
+ * of the replaced call sites did — a corrupt entry must not stop an app from
21452
+ * loading, and the user can always sign in again.
21453
+ */
21454
+ read() {
21455
+ const raw = this.readRaw();
21456
+ if (raw === void 0) return void 0;
21457
+ try {
21458
+ return JSON.parse(raw);
21459
+ } catch (e) {
21460
+ return void 0;
21461
+ }
21462
+ }
21463
+ write(session) {
21464
+ const storage = this.storage;
21465
+ if (!storage) return;
21466
+ storage[this.key] = JSON.stringify(session);
21467
+ }
21468
+ clear() {
21469
+ const storage = this.storage;
21470
+ if (!storage) return;
21471
+ delete storage[this.key];
21472
+ }
21473
+ /**
21474
+ * Read and write any key in the same storage, with the same absent-storage
21475
+ * tolerance.
21476
+ *
21477
+ * Here for Parse's installation id, which lives beside the session under
21478
+ * `Parse/<appId>/installationId` and is the only other thing `userservice.ts`
21479
+ * kept in local storage. It is not session state, so it gets no method of its
21480
+ * own — but it does need the SSR guard, and duplicating that guard is how the
21481
+ * four copies above happened in the first place.
21482
+ */
21483
+ readKey(key) {
21484
+ const storage = this.storage;
21485
+ if (!storage) return void 0;
21486
+ const value2 = storage[key];
21487
+ return value2 === void 0 || value2 === null ? void 0 : String(value2);
21488
+ }
21489
+ writeKey(key, value2) {
21490
+ const storage = this.storage;
21491
+ if (!storage) return;
21492
+ storage[key] = value2;
21493
+ }
21494
+ // ── Cross-tab ────────────────────────────────────────────────────────────
21495
+ /**
21496
+ * Hear about session changes made by **other** tabs.
21497
+ *
21498
+ * The `storage` event fires only in tabs that did not make the change, which
21499
+ * is exactly the semantics wanted and is why this is preferred to
21500
+ * `BroadcastChannel`: no echo to filter out, no availability question, and it
21501
+ * comes free with the storage already in use.
21502
+ *
21503
+ * Returns an unsubscribe function. Returns a no-op one when there is no
21504
+ * broadcaster, so callers never branch.
21505
+ */
21506
+ onExternalChange(handler) {
21507
+ const broadcaster = this.broadcaster;
21508
+ if (!broadcaster) return () => {
21509
+ };
21510
+ const listener = (event) => {
21511
+ if (event && event.key !== null && event.key !== this.key) return;
21512
+ handler(this.read());
21513
+ };
21514
+ broadcaster.addEventListener("storage", listener);
21515
+ return () => broadcaster.removeEventListener("storage", listener);
21516
+ }
21517
+ /**
21518
+ * Try to become the tab that refreshes.
21519
+ *
21520
+ * Read, write, **read back**. `localStorage` writes are serialised per origin,
21521
+ * so in the racing case both tabs write and both read back and exactly one
21522
+ * sees its own id — and the loser discovers that *before issuing any request*,
21523
+ * because the whole sequence is synchronous with no `await` in it. That is
21524
+ * what keeps the cost of a lost race at one tab doing nothing rather than two
21525
+ * tabs spending the same rotating refresh token.
21526
+ *
21527
+ * It is not a mutex and BCN-006-LIFECYCLE-DESIGN §5 does not claim it is. The
21528
+ * residual risk is a same-millisecond interleaving across processes on an
21529
+ * implementation that does not serialise writes; the cost if it happens is one
21530
+ * wasted refresh, and the follower's wait-then-adopt behaviour means the user
21531
+ * still ends up with the session the winner wrote.
21532
+ *
21533
+ * **The lock expires.** A tab closed mid-refresh must not hold it forever —
21534
+ * that would be worse than having no lock at all.
21535
+ */
21536
+ tryAcquireRefreshLock() {
21537
+ const storage = this.storage;
21538
+ if (!storage) return false;
21539
+ const now = this.now();
21540
+ const existing = this.readLock();
21541
+ if (existing && existing.owner !== this.tabId && existing.expiresAt > now) return false;
21542
+ storage[this.lockKey] = JSON.stringify({ owner: this.tabId, expiresAt: now + this.lockTtlMs });
21543
+ const confirmed = this.readLock();
21544
+ return confirmed !== void 0 && confirmed.owner === this.tabId;
21545
+ }
21546
+ /** Release only our own lock — never one another tab has since taken. */
21547
+ releaseRefreshLock() {
21548
+ const storage = this.storage;
21549
+ if (!storage) return;
21550
+ const existing = this.readLock();
21551
+ if (existing && existing.owner !== this.tabId) return;
21552
+ delete storage[this.lockKey];
21553
+ }
21554
+ readLock() {
21555
+ const raw = this.readKey(this.lockKey);
21556
+ if (raw === void 0) return void 0;
21557
+ try {
21558
+ const parsed = JSON.parse(raw);
21559
+ if (!parsed || typeof parsed.owner !== "string" || typeof parsed.expiresAt !== "number") return void 0;
21560
+ return parsed;
21561
+ } catch (e) {
21562
+ return void 0;
21563
+ }
21564
+ }
21565
+ };
21566
+ _parseStores = /* @__PURE__ */ new Map();
21567
+ }
21568
+ });
21569
+
21378
21570
  // ../nodegx-backend-contract/src/realtime.ts
21379
21571
  function nextReconnectDelay(attempt, timing = REALTIME_TIMING) {
21380
21572
  const n = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
@@ -21535,6 +21727,7 @@ var DEFAULT_TIMING, DEFAULT_HOST, RealtimeSubscription;
21535
21727
  var init_RealtimeSubscription = __esm({
21536
21728
  "../noodl-runtime/src/api/backends/realtime/RealtimeSubscription.ts"() {
21537
21729
  init_resolveBackend_pure();
21730
+ init_SessionStore();
21538
21731
  init_realtime2();
21539
21732
  DEFAULT_TIMING = REALTIME_TIMING;
21540
21733
  DEFAULT_HOST = {};
@@ -21775,9 +21968,20 @@ var init_RealtimeSubscription = __esm({
21775
21968
  * fallback.
21776
21969
  */
21777
21970
  get token() {
21778
- if (isParseWireType(this.handle.type)) return this.handle.sessionToken || "";
21971
+ if (isParseWireType(this.handle.type)) return this.handle.sessionToken || this.storedSession() || "";
21779
21972
  return this.handle.sessionToken || this.handle.publicToken || "";
21780
21973
  }
21974
+ /**
21975
+ * 🔴 P121-TWO005-LIVEANON. The signed-in user's session, where a Parse-wire app keeps it: the session store, keyed by
21976
+ * the app id in `publicToken` — the same place `ParseWireAdapter` reads it from for every request
21977
+ * (`readStoredCurrentUser`). Nothing sets `handle.sessionToken` on this wire, so reading the handle alone made every
21978
+ * subscription anonymous for a signed-in member, and a collection only members may read refused it ("denies find
21979
+ * to anonymous") on every retry. SBR-011's arm passed a session ON THE HANDLE, which a real app never has.
21980
+ */
21981
+ storedSession() {
21982
+ const stored = parseSessionStore(this.handle.publicToken).read();
21983
+ return stored && typeof stored.sessionToken === "string" ? stored.sessionToken : "";
21984
+ }
21781
21985
  _armDeadline(generation) {
21782
21986
  this._clearTimer("_deadlineTimer");
21783
21987
  this._deadlineTimer = this._setTimeout(() => {
@@ -22321,12 +22525,11 @@ var init_SseTransport = __esm({
22321
22525
  helloEvent: "connected",
22322
22526
  changeEvents: () => ["change"],
22323
22527
  resyncEvent: "resync",
22324
- subscribeRequest(base, token, clientId, subscriptions) {
22528
+ subscribeRequest(base, _token, clientId, subscriptions) {
22325
22529
  const body = subscriptions.map(
22326
22530
  (subscription) => subscription.where === void 0 ? { collection: subscription.collection } : { collection: subscription.collection, filter: subscription.where }
22327
22531
  );
22328
22532
  const headers = { "content-type": "application/json" };
22329
- if (token) headers["authorization"] = "Bearer " + token;
22330
22533
  return {
22331
22534
  url: base + "/realtime/subscriptions",
22332
22535
  init: {
@@ -24109,8 +24312,8 @@ var init_nodegx = __esm({
24109
24312
  // The header works. What it cannot do is make a file private on behalf of
24110
24313
  // nobody, and an author reading "supported" would not expect that.
24111
24314
  "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"
24315
+ "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.",
24316
+ "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
24317
  ),
24115
24318
  "files.progress": supported("XHR upload path reports progress"),
24116
24319
  "auth.password": supported("nodegx-backend/src/server/users.ts"),
@@ -25844,198 +26047,6 @@ var init_AdapterEvents = __esm({
25844
26047
  }
25845
26048
  });
25846
26049
 
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
26050
  // ../noodl-runtime/src/api/backends/fileRef.ts
26040
26051
  function jwtExpiry(token) {
26041
26052
  if (typeof token !== "string") return void 0;
@@ -26169,6 +26180,13 @@ function withStatus(err, status) {
26169
26180
  if (status !== void 0 && status > 0) envelope.status = status;
26170
26181
  return envelope;
26171
26182
  }
26183
+ function uploadHeaders(options) {
26184
+ const headers = {};
26185
+ if (options.private) headers["X-NodeGX-File-Private"] = "true";
26186
+ const readers = typeof options.readers === "string" ? options.readers.trim() : "";
26187
+ if (readers) headers["X-NodeGX-File-Readers"] = readers;
26188
+ return Object.keys(headers).length ? headers : void 0;
26189
+ }
26172
26190
  function _removeProtectedFields(data, className) {
26173
26191
  const _data = Object.assign({}, data);
26174
26192
  _protectedFields._common.forEach((f) => delete _data[f]);
@@ -26586,10 +26604,10 @@ var init_ParseWireAdapter = __esm({
26586
26604
  content: options.file,
26587
26605
  // Dead — `_makeRequest` never reads `contentType`. See the module note.
26588
26606
  contentType: options.file.type,
26589
- // BAK-006 follow-up: the Upload File node's Private input. The backend
26590
- // reads this exact header (files.ts, case-insensitively) to ACL the
26591
- // upload to its owner instead of leaving it public.
26592
- headers: options.private ? { "X-NodeGX-File-Private": "true" } : void 0,
26607
+ // BAK-006 follow-up: the Upload File node's Private input, and (P121
26608
+ // MEMBERSPHOTO) its Readers. The backend reads these headers to ACL the
26609
+ // upload to its owner, and to those roles, instead of leaving it public.
26610
+ headers: uploadHeaders(options),
26593
26611
  // BCN-007 step 1. This used to be
26594
26612
  // `Object.assign({}, options.data, response)` cast to `{name, url}` —
26595
26613
  // whatever the backend sent, spread over a caller-supplied object no
@@ -66748,9 +66766,9 @@ var init_navigation_stack = __esm({
66748
66766
  }
66749
66767
  });
66750
66768
 
66751
- // type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/own019-s4-pack/packages/noodl-viewer-react/src/types.ts
66769
+ // type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/l1-npm023/packages/noodl-viewer-react/src/types.ts
66752
66770
  var require_types2 = __commonJS({
66753
- "type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/own019-s4-pack/packages/noodl-viewer-react/src/types.ts"(exports2, module2) {
66771
+ "type-stub:/Users/richardosborne/vscode_projects/OpenNoodl-worktrees/l1-npm023/packages/noodl-viewer-react/src/types.ts"(exports2, module2) {
66754
66772
  var noop2 = new Proxy(function() {
66755
66773
  }, { get: () => noop2, apply: () => noop2 });
66756
66774
  module2.exports = new Proxy({}, { get: () => noop2 });
@@ -86381,6 +86399,76 @@ var init_openfilepicker = __esm({
86381
86399
  }
86382
86400
  });
86383
86401
 
86402
+ // ../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts
86403
+ function shrunkSize(width, height, largestSide) {
86404
+ const longer = Math.max(width, height);
86405
+ if (!(largestSide > 0) || !(longer > largestSide)) return void 0;
86406
+ const scale = largestSide / longer;
86407
+ return {
86408
+ width: Math.max(1, Math.round(width * scale)),
86409
+ height: Math.max(1, Math.round(height * scale))
86410
+ };
86411
+ }
86412
+ function shrunkType(type) {
86413
+ return KEEPS_ITS_TYPE.has(type) ? type : "image/jpeg";
86414
+ }
86415
+ function isShrinkable(type) {
86416
+ return typeof type === "string" && type.startsWith("image/") && !LEFT_ALONE.has(type);
86417
+ }
86418
+ function shrunkName(name2, fromType, toType) {
86419
+ if (fromType === toType) return name2;
86420
+ const ext = toType === "image/jpeg" ? "jpg" : toType.slice("image/".length);
86421
+ const dot = name2.lastIndexOf(".");
86422
+ return `${dot > 0 ? name2.slice(0, dot) : name2 || "photo"}.${ext}`;
86423
+ }
86424
+ function qualityOf(quality) {
86425
+ const q = typeof quality === "number" && quality > 0 && quality <= 100 ? quality : DEFAULT_PHOTO_QUALITY;
86426
+ return q / 100;
86427
+ }
86428
+ async function shrinkPhoto(file, options, ops = browserPhotoOps) {
86429
+ if (!(options.largestSide > 0) || !isShrinkable(file.type)) return file;
86430
+ let photo;
86431
+ try {
86432
+ photo = await ops.decode(file);
86433
+ const size = shrunkSize(photo.width, photo.height, options.largestSide);
86434
+ if (!size) return file;
86435
+ const type = shrunkType(file.type);
86436
+ const blob = await ops.encode(photo, size.width, size.height, type, qualityOf(options.quality));
86437
+ if (!blob || blob.size === 0 || blob.size >= file.size) return file;
86438
+ return new File([blob], shrunkName(file.name, file.type, type), {
86439
+ type,
86440
+ lastModified: file.lastModified
86441
+ });
86442
+ } catch (_e) {
86443
+ return file;
86444
+ } finally {
86445
+ if (photo && photo.close) photo.close();
86446
+ }
86447
+ }
86448
+ var DEFAULT_PHOTO_QUALITY, KEEPS_ITS_TYPE, LEFT_ALONE, browserPhotoOps;
86449
+ var init_shrinkphoto = __esm({
86450
+ "../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts"() {
86451
+ DEFAULT_PHOTO_QUALITY = 80;
86452
+ KEEPS_ITS_TYPE = /* @__PURE__ */ new Set(["image/png", "image/webp", "image/jpeg"]);
86453
+ LEFT_ALONE = /* @__PURE__ */ new Set(["image/gif", "image/svg+xml"]);
86454
+ browserPhotoOps = {
86455
+ async decode(file) {
86456
+ return createImageBitmap(file, { imageOrientation: "from-image" });
86457
+ },
86458
+ encode(photo, width, height, type, quality) {
86459
+ const canvas = document.createElement("canvas");
86460
+ canvas.width = width;
86461
+ canvas.height = height;
86462
+ const ctx = canvas.getContext("2d");
86463
+ if (!ctx) return Promise.resolve(null);
86464
+ ctx.imageSmoothingQuality = "high";
86465
+ ctx.drawImage(photo, 0, 0, width, height);
86466
+ return new Promise((resolve) => canvas.toBlob(resolve, type, quality));
86467
+ }
86468
+ };
86469
+ }
86470
+ });
86471
+
86384
86472
  // ../noodl-viewer-react/src/nodes/std-library/uploadfile.ts
86385
86473
  var uploadfile_exports = {};
86386
86474
  __export(uploadfile_exports, {
@@ -86401,6 +86489,7 @@ var init_uploadfile = __esm({
86401
86489
  init_schema_ports();
86402
86490
  init_fileRef();
86403
86491
  init_outcome();
86492
+ init_shrinkphoto();
86404
86493
  UPLOAD_ERROR_CODE = "upload-file/upload-failed";
86405
86494
  UploadFile = {
86406
86495
  name: "Upload File",
@@ -86439,6 +86528,42 @@ var init_uploadfile = __esm({
86439
86528
  this._internal.private = value2;
86440
86529
  }
86441
86530
  },
86531
+ // P121-TWO001-MEMBERSPHOTO: Private alone is the uploader's alone, and a feed shows every
86532
+ // member's photos to every other member. `X-NodeGX-File-Readers` shares a private file with
86533
+ // roles; the backend refuses a malformed list, or a role the uploader does not hold, in words.
86534
+ readers: {
86535
+ group: "General",
86536
+ displayName: "Readers",
86537
+ type: "string",
86538
+ 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",
86539
+ set(value2) {
86540
+ this._internal.readers = value2;
86541
+ }
86542
+ },
86543
+ // ── P121-TWO008-UPLOADSHRINK: Shrink Photos ──────────────────────────
86544
+ //
86545
+ // A phone photo is 3 to 8 MB, and the backend's thumbnails shrink what is served, not what
86546
+ // is sent. Off unless set, so every existing graph sends exactly what it sent before.
86547
+ shrinkTo: {
86548
+ group: "Shrink Photos",
86549
+ displayName: "Shrink To",
86550
+ type: "number",
86551
+ default: 0,
86552
+ 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",
86553
+ set(value2) {
86554
+ this._internal.shrinkTo = value2;
86555
+ }
86556
+ },
86557
+ photoQuality: {
86558
+ group: "Shrink Photos",
86559
+ displayName: "Photo Quality",
86560
+ type: "number",
86561
+ default: 80,
86562
+ 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",
86563
+ set(value2) {
86564
+ this._internal.photoQuality = value2;
86565
+ }
86566
+ },
86442
86567
  // ── BCN-007 step 3: File Location ────────────────────────────────────
86443
86568
  //
86444
86569
  // ⚠️ **Two of the five backends cannot store a file without being told
@@ -86516,10 +86641,11 @@ var init_uploadfile = __esm({
86516
86641
  }
86517
86642
  const store = this.cloudStore([outcome]);
86518
86643
  if (!store) return;
86519
- store.uploadFile({
86520
- file,
86644
+ const send = (sent) => store.uploadFile({
86645
+ file: sent,
86521
86646
  private: this._internal.private,
86522
- target: this.fileTarget(),
86647
+ readers: this._internal.readers,
86648
+ target: this.fileTarget(sent),
86523
86649
  onUploadProgress: (p) => {
86524
86650
  this._internal.progressTotal = p.total;
86525
86651
  this._internal.progressLoaded = p.loaded;
@@ -86535,14 +86661,20 @@ var init_uploadfile = __esm({
86535
86661
  // structurally rather than imported, because `cloudstore.js` is still
86536
86662
  // JavaScript and declares this callback `unknown` (PLAT-006 residual).
86537
86663
  success: (response) => {
86538
- const sent = file && typeof file.name === "string" ? file.name : void 0;
86539
- const filename = response.filename !== void 0 ? response.filename : sent;
86664
+ const sentName = sent && typeof sent.name === "string" ? sent.name : void 0;
86665
+ const filename = response.filename !== void 0 ? response.filename : sentName;
86540
86666
  this._internal.cloudFile = new import_cloudfile.default(filename !== void 0 ? Object.assign({}, response, { filename }) : response);
86541
86667
  this.flagOutputDirty("cloudFile");
86542
86668
  this.reportOutcome(outcome, "done");
86543
86669
  },
86544
86670
  error: (e) => this.setError(e, [outcome])
86545
86671
  });
86672
+ const shrinkTo = this._internal.shrinkTo;
86673
+ if (typeof shrinkTo === "number" && shrinkTo > 0 && isShrinkable(file.type)) {
86674
+ shrinkPhoto(file, { largestSide: shrinkTo, quality: this._internal.photoQuality }).then(send);
86675
+ } else {
86676
+ send(file);
86677
+ }
86546
86678
  });
86547
86679
  }
86548
86680
  }
@@ -86690,8 +86822,9 @@ var init_uploadfile = __esm({
86690
86822
  * meaningful: a project has one backend selected, and only one of the two
86691
86823
  * backends reads either group.
86692
86824
  */
86693
- fileTarget() {
86694
- const { bucket, path: path2, collection, recordId, field, file } = this._internal;
86825
+ fileTarget(sent) {
86826
+ const { bucket, path: path2, collection, recordId, field } = this._internal;
86827
+ const file = sent || this._internal.file;
86695
86828
  if (bucket) {
86696
86829
  return { kind: "bucket", bucket, path: path2 || file?.name || "" };
86697
86830
  }
@@ -452,6 +452,7 @@
452
452
  "../noodl-viewer-react/src/nodes/std-library/openfilepicker.ts",
453
453
  "../noodl-viewer-react/src/nodes/std-library/repeat.ts",
454
454
  "../noodl-viewer-react/src/nodes/std-library/screenresolution.ts",
455
+ "../noodl-viewer-react/src/nodes/std-library/shrinkphoto.ts",
455
456
  "../noodl-viewer-react/src/nodes/std-library/states.ts",
456
457
  "../noodl-viewer-react/src/nodes/std-library/timer.ts",
457
458
  "../noodl-viewer-react/src/nodes/std-library/uploadfile.ts",