@nodegx/preview 0.2.5 → 0.2.6

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.
@@ -18277,6 +18277,27 @@ var init_run_on_value_change = __esm({
18277
18277
  }
18278
18278
  });
18279
18279
 
18280
+ // ../noodl-runtime/src/tracecause.ts
18281
+ function currentTraceCause(context) {
18282
+ const c = context;
18283
+ return c && c.traceEnabled ? c._currentCause || 0 : 0;
18284
+ }
18285
+ function withTraceCause(context, cause, fn) {
18286
+ const c = context;
18287
+ if (!cause || !c || !c.traceEnabled) return fn();
18288
+ const previous = c._currentCause;
18289
+ c._currentCause = cause;
18290
+ try {
18291
+ return fn();
18292
+ } finally {
18293
+ c._currentCause = previous;
18294
+ }
18295
+ }
18296
+ var init_tracecause = __esm({
18297
+ "../noodl-runtime/src/tracecause.ts"() {
18298
+ }
18299
+ });
18300
+
18280
18301
  // ../noodl-runtime/src/outputproperty.ts
18281
18302
  var require_outputproperty = __commonJS({
18282
18303
  "../noodl-runtime/src/outputproperty.ts"(exports2, module2) {
@@ -18452,6 +18473,7 @@ var require_node2 = __commonJS({
18452
18473
  init_diagnostics();
18453
18474
  init_outcome();
18454
18475
  init_run_on_value_change();
18476
+ init_tracecause();
18455
18477
  var OutputProperty = require_outputproperty();
18456
18478
  var NAN_INPUT_KEY = "node/nan-input/";
18457
18479
  var SIGNAL_PULSE = Object.freeze({ __noodlSignalPulse: true });
@@ -18964,6 +18986,8 @@ var require_node2 = __commonJS({
18964
18986
  });
18965
18987
  Node17.prototype.beginOutcome = function(inputData) {
18966
18988
  const token = { reported: void 0 };
18989
+ const cause = currentTraceCause(this.context);
18990
+ if (cause) token.cause = cause;
18967
18991
  const scope = this.nodeScope;
18968
18992
  const runContext = scope && scope.runContext;
18969
18993
  if (runContext && runContext.beginStep) {
@@ -18999,6 +19023,10 @@ var require_node2 = __commonJS({
18999
19023
  return;
19000
19024
  }
19001
19025
  token.reported = outcome;
19026
+ const traceContext = this.context;
19027
+ const tokenCause = token.cause;
19028
+ const causeBefore = traceContext ? traceContext._currentCause : 0;
19029
+ if (tokenCause && traceContext && traceContext.traceEnabled) traceContext._currentCause = tokenCause;
19002
19030
  this._raisingForOutcome = true;
19003
19031
  try {
19004
19032
  if (outcome === "failure" && !(options && options.raise === false)) {
@@ -19028,6 +19056,7 @@ var require_node2 = __commonJS({
19028
19056
  }
19029
19057
  } finally {
19030
19058
  this._raisingForOutcome = false;
19059
+ if (tokenCause && traceContext && traceContext.traceEnabled) traceContext._currentCause = causeBefore;
19031
19060
  }
19032
19061
  const step = token.step;
19033
19062
  if (step !== void 0 && step !== null) {
@@ -19123,7 +19152,12 @@ var require_node2 = __commonJS({
19123
19152
  this.flagDirty();
19124
19153
  };
19125
19154
  Node17.prototype.scheduleAfterInputsHaveUpdated = function(callback) {
19126
- this._afterInputsHaveUpdatedCallbacks.push(callback);
19155
+ const cause = currentTraceCause(this.context);
19156
+ this._afterInputsHaveUpdatedCallbacks.push(
19157
+ cause ? function() {
19158
+ return withTraceCause(this.context, cause, () => callback.call(this));
19159
+ } : callback
19160
+ );
19127
19161
  this.flagDirty();
19128
19162
  };
19129
19163
  Node17.prototype.setNodeModel = function(nodeModel) {
@@ -19226,6 +19260,60 @@ var require_node2 = __commonJS({
19226
19260
  }
19227
19261
  });
19228
19262
 
19263
+ // ../nodegx-project-contract/renamed-ports.ts
19264
+ function renamedOutput(typeName4, port5) {
19265
+ const renames = own.call(RENAMED_OUTPUTS, typeName4) ? RENAMED_OUTPUTS[typeName4] : void 0;
19266
+ return renames && own.call(renames, port5) ? renames[port5] : void 0;
19267
+ }
19268
+ var RENAMED_OUTPUTS, own;
19269
+ var init_renamed_ports = __esm({
19270
+ "../nodegx-project-contract/renamed-ports.ts"() {
19271
+ "use strict";
19272
+ RENAMED_OUTPUTS = {
19273
+ AddDbModelRelation: { relationAdded: "done" },
19274
+ Boolean: { stored: "completed" },
19275
+ CloudFunction2: { success: "done" },
19276
+ CollectionClear: { modified: "done" },
19277
+ CollectionInsert: { modified: "done" },
19278
+ CollectionNew: { created: "done" },
19279
+ CollectionRemove: { modified: "done" },
19280
+ Color: { stored: "completed" },
19281
+ DeleteDbModelProperties: { deleted: "done" },
19282
+ "Event Sender": { sent: "done" },
19283
+ NavigationClosePopup: { success: "done" },
19284
+ NewDbModelProperties: { created: "done" },
19285
+ NewModel: { created: "done" },
19286
+ Number: { stored: "completed" },
19287
+ "Open File Picker": { success: "done", cancelled: "unchanged" },
19288
+ PageStackNavigate: { navigated: "done" },
19289
+ PageStackNavigateBack: { success: "done" },
19290
+ PageStackNavigateToPath: { success: "done" },
19291
+ RemoveDbModelRelation: { relationRemoved: "done" },
19292
+ RouterNavigate: { navigated: "done" },
19293
+ RunTasks: { success: "done" },
19294
+ SetDbModelProperties: { stored: "done" },
19295
+ SetModelProperties: { stored: "done" },
19296
+ "Sign File URL": { success: "done" },
19297
+ String: { stored: "completed" },
19298
+ "Unique Id": { generated: "done" },
19299
+ "Upload File": { success: "done" },
19300
+ "net.noodl.HTTP": { success: "done" },
19301
+ "net.noodl.SetComponentObjectProperties": { stored: "done" },
19302
+ "net.noodl.SetParentComponentObjectProperties": { stored: "done" },
19303
+ "net.noodl.externallink": { success: "done" },
19304
+ "net.noodl.user.LogIn": { success: "done" },
19305
+ "net.noodl.user.LogOut": { success: "done" },
19306
+ "net.noodl.user.RequestMagicLink": { success: "done" },
19307
+ "net.noodl.user.SetUserProperties": { success: "done" },
19308
+ "net.noodl.user.SignInWith": { success: "done" },
19309
+ "net.noodl.user.SignUp": { success: "done" },
19310
+ "noodl.cloud.response": { sent: "done" },
19311
+ "noodl.cloud.sendemail": { sent: "done", failed: "failure" }
19312
+ };
19313
+ own = Object.prototype.hasOwnProperty;
19314
+ }
19315
+ });
19316
+
19229
19317
  // ../noodl-runtime/src/guid.ts
19230
19318
  var require_guid = __commonJS({
19231
19319
  "../noodl-runtime/src/guid.ts"(exports2, module2) {
@@ -19243,6 +19331,7 @@ var require_guid = __commonJS({
19243
19331
  var require_nodescope = __commonJS({
19244
19332
  "../noodl-runtime/src/nodescope.ts"(exports2, module2) {
19245
19333
  "use strict";
19334
+ init_renamed_ports();
19246
19335
  var guid5 = require_guid();
19247
19336
  function hasNode(map2, id) {
19248
19337
  return Object.prototype.hasOwnProperty.call(map2, id);
@@ -19275,11 +19364,15 @@ var require_nodescope = __commonJS({
19275
19364
  var sourceNode = this.getNodeWithId(connectionData.sourceId), targetNode = this.getNodeWithId(connectionData.targetId);
19276
19365
  targetNode.registerInputIfNeeded(connectionData.targetPort);
19277
19366
  sourceNode.registerOutputIfNeeded(connectionData.sourcePort);
19278
- targetNode.connectInput(connectionData.targetPort, sourceNode, connectionData.sourcePort);
19367
+ targetNode.connectInput(connectionData.targetPort, sourceNode, sourcePortOf(sourceNode, connectionData.sourcePort));
19279
19368
  } catch (e) {
19280
19369
  console.error(e.message);
19281
19370
  }
19282
19371
  };
19372
+ function sourcePortOf(sourceNode, port5) {
19373
+ if (sourceNode.hasOutput(port5)) return port5;
19374
+ return renamedOutput(sourceNode.name, port5) ?? port5;
19375
+ }
19283
19376
  NodeScope.prototype.setNodeParameters = function(node, nodeModel) {
19284
19377
  const variant = this.context.variants.getVariant(nodeModel.type, nodeModel.variant);
19285
19378
  if (variant) {
@@ -19287,10 +19380,10 @@ var require_nodescope = __commonJS({
19287
19380
  } else {
19288
19381
  const parameters = nodeModel.parameters;
19289
19382
  var inputNames = Object.keys(parameters);
19290
- const RUN_ON_CHANGE_PREFIX3 = "runOnChange-";
19383
+ const RUN_ON_CHANGE_PREFIX4 = "runOnChange-";
19291
19384
  const metadata = this.context.nodeRegister.hasNode(node.name) ? this.context.nodeRegister.getNodeMetadata(node.name) : void 0;
19292
19385
  const priorityOf = function(name2) {
19293
- if (name2.indexOf(RUN_ON_CHANGE_PREFIX3) === 0) return Number.MAX_SAFE_INTEGER;
19386
+ if (name2.indexOf(RUN_ON_CHANGE_PREFIX4) === 0) return Number.MAX_SAFE_INTEGER;
19294
19387
  const input2 = metadata ? metadata.inputs[name2] : void 0;
19295
19388
  return input2 ? input2.inputPriority : 0;
19296
19389
  };
@@ -19464,7 +19557,9 @@ var require_nodescope = __commonJS({
19464
19557
  };
19465
19558
  NodeScope.prototype.removeConnection = function(connectionModel) {
19466
19559
  var targetNode = this.getNodeWithId(connectionModel.targetId);
19467
- targetNode.removeInputConnection(connectionModel.targetPort, connectionModel.sourceId, connectionModel.sourcePort);
19560
+ const sourceNode = this.hasNodeWithId(connectionModel.sourceId) ? this.getNodeWithId(connectionModel.sourceId) : void 0;
19561
+ const sourcePort = sourceNode ? sourcePortOf(sourceNode, connectionModel.sourcePort) : connectionModel.sourcePort;
19562
+ targetNode.removeInputConnection(connectionModel.targetPort, connectionModel.sourceId, sourcePort);
19468
19563
  };
19469
19564
  NodeScope.prototype.setComponentModel = async function(componentModel) {
19470
19565
  this.componentModel = componentModel;
@@ -27078,7 +27173,9 @@ var init_function_ports = __esm({
27078
27173
  INPUT_PATTERNS = [/Inputs\.([A-Za-z0-9_]+)/g, /Inputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]/g];
27079
27174
  SIGNAL_PATTERNS = [
27080
27175
  /Outputs\.([A-Za-z0-9_]+)\s*\(\s*\)/g,
27081
- /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\(\s*\)/g
27176
+ /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\(\s*\)/g,
27177
+ /Outputs\.([A-Za-z0-9_]+)\.send\s*\(\s*\)/g,
27178
+ /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\.send\s*\(\s*\)/g
27082
27179
  ];
27083
27180
  OUTPUT_PATTERNS = [/Outputs\.([A-Za-z0-9_]+)/g, /Outputs\s*\[\s*"([^"]*)"\s*\]/g];
27084
27181
  REGEX_AFTER = /* @__PURE__ */ new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "<", ">", "~", "^"]);
@@ -27095,6 +27192,7 @@ var require_simplejavascript = __commonJS({
27095
27192
  init_functionDiagnostics();
27096
27193
  init_scriptfiles();
27097
27194
  init_function_ports();
27195
+ init_tracecause();
27098
27196
  var Node17 = require_node2();
27099
27197
  var JavascriptNodeParser4 = require_javascriptnodeparser();
27100
27198
  var { logJavaScriptNodeError: logJavaScriptNodeError2, thrownMessage: thrownMessage3 } = (init_utils(), __toCommonJS(utils_exports));
@@ -27464,11 +27562,12 @@ var require_simplejavascript = __commonJS({
27464
27562
  }
27465
27563
  const inputs = this._internal.inputValues;
27466
27564
  const outputs2 = this._internal.outputValuesProxy;
27565
+ const runCause = currentTraceCause(this.context);
27467
27566
  placingSignalPorts(this).forEach((key) => {
27468
27567
  const _sendSignal = () => {
27469
27568
  this._internal.outputWrites.add(key.substring("out-".length));
27470
27569
  publishHeldWrites(this);
27471
- if (this.hasOutput(key)) this.sendSignalOnOutput(key);
27570
+ if (this.hasOutput(key)) withTraceCause(this.context, runCause, () => this.sendSignalOnOutput(key));
27472
27571
  };
27473
27572
  const signalValue = _sendSignal;
27474
27573
  signalValue.send = _sendSignal;
@@ -27493,7 +27592,7 @@ var require_simplejavascript = __commonJS({
27493
27592
  this._internal.lastError = "";
27494
27593
  this.flagOutputDirty("error");
27495
27594
  }
27496
- this.sendSignalOnOutput("success");
27595
+ withTraceCause(this.context, runCause, () => this.sendSignalOnOutput("success"));
27497
27596
  reportOutcomes(this, tokens, "done");
27498
27597
  }
27499
27598
  } catch (e) {
@@ -35613,6 +35712,466 @@ var init_TokenLifecycle = __esm({
35613
35712
  }
35614
35713
  });
35615
35714
 
35715
+ // ../noodl-runtime/src/api/backends/ParseAuthAdapter.ts
35716
+ function pageSays(response, phrase) {
35717
+ return typeof response === "string" && response.indexOf(phrase) !== -1;
35718
+ }
35719
+ function htmlPageError(error, invalidPhrase, invalidMessage, fallback) {
35720
+ if (typeof error !== "string" || error.indexOf("<") === -1) return error || fallback;
35721
+ return error.indexOf(invalidPhrase) !== -1 ? invalidMessage : fallback;
35722
+ }
35723
+ function sessionTokenOf(session) {
35724
+ return session ? session.sessionToken : void 0;
35725
+ }
35726
+ var guid, HANDOFF_PARAM, AUTH_ERROR_PARAM, SESSION_MOVED_MESSAGE, ParseAuthAdapter;
35727
+ var init_ParseAuthAdapter = __esm({
35728
+ "../noodl-runtime/src/api/backends/ParseAuthAdapter.ts"() {
35729
+ init_AuthEvents();
35730
+ init_SessionStore();
35731
+ init_TokenLifecycle();
35732
+ guid = require_guid();
35733
+ HANDOFF_PARAM = "nodegx_auth";
35734
+ AUTH_ERROR_PARAM = "nodegx_auth_error";
35735
+ SESSION_MOVED_MESSAGE = "Someone signed in or out while this was being checked, so its answer was not used. Check again.";
35736
+ ParseAuthAdapter = class extends AuthEvents {
35737
+ constructor(options) {
35738
+ super();
35739
+ /** BAK-004: the state of a sign-in returning from a provider or a magic link. */
35740
+ this.oauthReturn = { inProgress: false };
35741
+ /**
35742
+ * One store and one controller per backend, keyed by application id.
35743
+ *
35744
+ * Per handle rather than per adapter because the contract's whole point is
35745
+ * that a project may have more than one backend, and two backends have two
35746
+ * sessions — the phase decided explicitly that they are never shared.
35747
+ */
35748
+ this.stores = /* @__PURE__ */ new Map();
35749
+ this.controllers = /* @__PURE__ */ new Map();
35750
+ this.serializeObject = options.serializeObject;
35751
+ this.createStore = options.createStore || parseSessionStore;
35752
+ this.lifecycle = options.lifecycle || { kind: "eternal" };
35753
+ }
35754
+ // ── Session storage ──────────────────────────────────────────────────────
35755
+ sessionStore(handle) {
35756
+ const appId = handle.publicToken;
35757
+ const cacheKey = String(appId);
35758
+ let store2 = this.stores.get(cacheKey);
35759
+ if (store2 === void 0) {
35760
+ store2 = this.createStore(appId);
35761
+ this.stores.set(cacheKey, store2);
35762
+ }
35763
+ return store2;
35764
+ }
35765
+ /**
35766
+ * The lifecycle controller for this backend.
35767
+ *
35768
+ * On Parse this is an `eternal` controller: it arms no timer, subscribes to no
35769
+ * storage events, and its gate is a synchronous pass-through. It is on the
35770
+ * request path anyway, so that BCN-004 adds a `performRefresh` rather than a
35771
+ * call site.
35772
+ */
35773
+ lifecycleController(handle) {
35774
+ const cacheKey = String(handle.publicToken);
35775
+ let controller = this.controllers.get(cacheKey);
35776
+ if (controller === void 0) {
35777
+ controller = new TokenLifecycleController({
35778
+ lifecycle: this.lifecycle,
35779
+ store: this.sessionStore(handle),
35780
+ onSessionLost: (reason) => {
35781
+ this.emitAuthEvent("sessionChanged", void 0);
35782
+ this.emitAuthEvent("sessionLost", reason);
35783
+ },
35784
+ onSessionClearedElsewhere: () => {
35785
+ this.emitAuthEvent("sessionChanged", void 0);
35786
+ this.emitAuthEvent("loggedOut");
35787
+ }
35788
+ });
35789
+ controller.start();
35790
+ this.controllers.set(cacheKey, controller);
35791
+ }
35792
+ return controller;
35793
+ }
35794
+ /** The stored session, or `undefined`. Not a contract method. */
35795
+ getCurrentUser(handle) {
35796
+ return this.sessionStore(handle).read();
35797
+ }
35798
+ setSession(handle, session) {
35799
+ this.sessionStore(handle).write(session);
35800
+ this.lifecycleController(handle).sessionChanged();
35801
+ this.emitAuthEvent("sessionChanged", session);
35802
+ }
35803
+ clearSession(handle) {
35804
+ this.sessionStore(handle).clear();
35805
+ this.lifecycleController(handle).sessionChanged();
35806
+ this.emitAuthEvent("sessionChanged", void 0);
35807
+ }
35808
+ // ── The wire ─────────────────────────────────────────────────────────────
35809
+ /**
35810
+ * The Parse auth seam. Moved from `userservice.ts:198` with one substitution:
35811
+ * `this.endpoint`/`this.appId` became `handle.url`/`handle.publicToken`.
35812
+ *
35813
+ * It is deliberately *not* `ParseWireAdapter._makeRequest`. The two differ in
35814
+ * three ways that all matter: this one sends `X-Parse-Installation-Id`, never
35815
+ * sends a master key, and hands its error callback Parse's `code` (the `209`
35816
+ * the session check turns on). Merging them would be a rewrite in a task
35817
+ * whose value is that nothing was rewritten.
35818
+ */
35819
+ _makeRequest(handle, path10, options) {
35820
+ if (!handle.url) {
35821
+ if (options.error) {
35822
+ options.error({ error: "No active cloud service", status: 0 });
35823
+ }
35824
+ return;
35825
+ }
35826
+ const store2 = this.sessionStore(handle);
35827
+ this.lifecycleController(handle).withSession((session) => {
35828
+ const xhr = new XMLHttpRequest();
35829
+ xhr.onreadystatechange = function() {
35830
+ if (xhr.readyState === 4) {
35831
+ let json2;
35832
+ try {
35833
+ json2 = JSON.parse(xhr.response);
35834
+ } catch (e) {
35835
+ }
35836
+ if (xhr.status === 200 || xhr.status === 201) {
35837
+ options.success(json2 || xhr.response);
35838
+ } else options.error(json2 || { error: xhr.responseText, status: xhr.status });
35839
+ }
35840
+ };
35841
+ xhr.open(options.method || "GET", handle.url + path10, true);
35842
+ xhr.setRequestHeader("X-Parse-Application-Id", handle.publicToken);
35843
+ const iidKey = parseInstallationIdKey(handle.publicToken);
35844
+ let _iid = store2.readKey(iidKey);
35845
+ if (_iid === void 0) {
35846
+ _iid = guid();
35847
+ store2.writeKey(iidKey, _iid);
35848
+ }
35849
+ xhr.setRequestHeader("X-Parse-Installation-Id", _iid);
35850
+ if (options.sessionToken) xhr.setRequestHeader("X-Parse-Session-Token", options.sessionToken);
35851
+ else if (session !== void 0) {
35852
+ xhr.setRequestHeader("X-Parse-Session-Token", session.sessionToken);
35853
+ }
35854
+ xhr.setRequestHeader("Content-Type", "application/json");
35855
+ xhr.send(JSON.stringify(options.content));
35856
+ });
35857
+ }
35858
+ // ── The ten ──────────────────────────────────────────────────────────────
35859
+ logIn(handle, options) {
35860
+ this._makeRequest(handle, "/login", {
35861
+ method: "POST",
35862
+ content: {
35863
+ username: options.username,
35864
+ password: options.password,
35865
+ _method: "GET"
35866
+ },
35867
+ success: (response) => {
35868
+ this.setSession(handle, response);
35869
+ options.success(response);
35870
+ this.emitAuthEvent("loggedIn", response);
35871
+ },
35872
+ error: (e) => {
35873
+ options.error(e.error);
35874
+ }
35875
+ });
35876
+ }
35877
+ logOut(handle, options) {
35878
+ this._makeRequest(handle, "/logout", {
35879
+ method: "POST",
35880
+ content: {},
35881
+ success: () => {
35882
+ this.clearSession(handle);
35883
+ options.success();
35884
+ this.emitAuthEvent("loggedOut");
35885
+ },
35886
+ error: (e) => {
35887
+ options.error(e.error);
35888
+ }
35889
+ });
35890
+ }
35891
+ signUp(handle, options) {
35892
+ const additionalUserProps = options.properties ? this.serializeObject({ ...options.properties }, "_User") : {};
35893
+ this._makeRequest(handle, "/users", {
35894
+ method: "POST",
35895
+ content: Object.assign({}, additionalUserProps, {
35896
+ username: options.username,
35897
+ password: options.password,
35898
+ email: options.email
35899
+ }),
35900
+ success: (response) => {
35901
+ const identity2 = { username: options.username };
35902
+ if (options.email !== void 0) identity2.email = options.email;
35903
+ const _cu = Object.assign(response, identity2, options.properties);
35904
+ if (_cu.emailVerified === void 0) _cu.emailVerified = false;
35905
+ this.setSession(handle, _cu);
35906
+ options.success(response);
35907
+ this.emitAuthEvent("loggedIn", _cu);
35908
+ },
35909
+ error: (e) => {
35910
+ options.error(e.error);
35911
+ }
35912
+ });
35913
+ }
35914
+ fetchCurrentUser(handle, options) {
35915
+ const asked = sessionTokenOf(this.getCurrentUser(handle));
35916
+ this._makeRequest(handle, "/users/me", {
35917
+ method: "GET",
35918
+ sessionToken: options.sessionToken,
35919
+ success: (response) => {
35920
+ if (sessionTokenOf(this.getCurrentUser(handle)) !== asked) {
35921
+ options.error(SESSION_MOVED_MESSAGE);
35922
+ return;
35923
+ }
35924
+ this.setSession(handle, response);
35925
+ this.emitAuthEvent("sessionGained", response);
35926
+ options.success(response);
35927
+ },
35928
+ error: (e) => {
35929
+ if (sessionTokenOf(this.getCurrentUser(handle)) !== asked) {
35930
+ options.error(SESSION_MOVED_MESSAGE);
35931
+ return;
35932
+ }
35933
+ if (e.code === 209) {
35934
+ this.sessionStore(handle).clear();
35935
+ this.emitAuthEvent("sessionLost");
35936
+ }
35937
+ options.error(e.error);
35938
+ }
35939
+ });
35940
+ }
35941
+ verifyEmail(handle, options) {
35942
+ this._makeRequest(
35943
+ handle,
35944
+ "/apps/" + handle.publicToken + "/verify_email?username=" + options.username + "&token=" + options.token,
35945
+ {
35946
+ method: "GET",
35947
+ // This endpoint answers with an HTML page rather than JSON, so the outcome has to be
35948
+ // read out of the page's text.
35949
+ success: (response) => {
35950
+ if (pageSays(response, "Successfully verified your email")) {
35951
+ options.success();
35952
+ } else if (pageSays(response, "Invalid Verification Link")) {
35953
+ options.error("Invalid verification token");
35954
+ } else {
35955
+ options.error("Failed to verify email");
35956
+ }
35957
+ },
35958
+ error: (e) => {
35959
+ options.error(htmlPageError(e.error, "Invalid Verification Link", "Invalid verification token", "Failed to verify email"));
35960
+ }
35961
+ }
35962
+ );
35963
+ }
35964
+ sendEmailVerification(handle, options) {
35965
+ this._makeRequest(handle, "/verificationEmailRequest", {
35966
+ method: "POST",
35967
+ content: { email: options.email },
35968
+ success: () => {
35969
+ options.success();
35970
+ },
35971
+ error: (e) => {
35972
+ options.error(e.error);
35973
+ }
35974
+ });
35975
+ }
35976
+ resetPassword(handle, options) {
35977
+ this._makeRequest(handle, "/apps/" + handle.publicToken + "/request_password_reset", {
35978
+ method: "POST",
35979
+ content: {
35980
+ username: options.username,
35981
+ token: options.token,
35982
+ new_password: options.newPassword
35983
+ },
35984
+ success: (response) => {
35985
+ if (pageSays(response, "Password successfully reset") || pageSays(response, "Successfully updated your password")) {
35986
+ options.success();
35987
+ } else if (pageSays(response, "Invalid Link")) {
35988
+ options.error("Invalid or expired reset link");
35989
+ } else {
35990
+ options.error("Failed to reset password");
35991
+ }
35992
+ },
35993
+ error: (e) => {
35994
+ options.error(htmlPageError(e.error, "Invalid Link", "Invalid or expired reset link", "Failed to reset password"));
35995
+ }
35996
+ });
35997
+ }
35998
+ requestPasswordReset(handle, options) {
35999
+ this._makeRequest(handle, "/requestPasswordReset", {
36000
+ method: "POST",
36001
+ content: { email: options.email },
36002
+ success: () => {
36003
+ options.success();
36004
+ },
36005
+ error: (e) => {
36006
+ options.error(e.error);
36007
+ }
36008
+ });
36009
+ }
36010
+ /**
36011
+ * Send the browser to a provider. This NAVIGATES AWAY: nothing after it runs,
36012
+ * and the result arrives on a later page load through {@link consumeAuthReturn}.
36013
+ *
36014
+ * `redirect` defaults to the current page (minus any leftover sign-in
36015
+ * parameters), so the user comes back where they were. The backend refuses
36016
+ * any target that is neither same-origin nor on its `redirectAllowList` — if
36017
+ * sign-in dead-ends with "origin not allowed", that list is the fix.
36018
+ */
36019
+ signInWithProvider(handle, options) {
36020
+ if (!handle.url) {
36021
+ if (options.error) options.error("No active cloud service");
36022
+ return;
36023
+ }
36024
+ if (!options.provider) {
36025
+ if (options.error) options.error("Sign In With: no provider was set.");
36026
+ return;
36027
+ }
36028
+ const redirect = options.redirect || this._currentUrlWithoutAuthParams();
36029
+ const url = `${handle.url}/oauth/${encodeURIComponent(options.provider)}/start?redirect=${encodeURIComponent(redirect)}`;
36030
+ window.location.href = url;
36031
+ }
36032
+ /**
36033
+ * Ask the backend to email a one-click sign-in link.
36034
+ *
36035
+ * Succeeds identically for a known and an unknown address — the endpoint is
36036
+ * anonymous and anti-enumerating by design, so "success" here means "the
36037
+ * request was accepted", never "an account exists". The node's help text says
36038
+ * so, because a UI that claims "check your inbox" only for real accounts
36039
+ * re-creates the oracle the endpoint removed.
36040
+ */
36041
+ requestMagicLink(handle, options) {
36042
+ this._makeRequest(handle, "/auth/magic-link", {
36043
+ method: "POST",
36044
+ content: { email: options.email, redirect: options.redirect || this._currentUrlWithoutAuthParams() },
36045
+ success: () => options.success(),
36046
+ error: (e) => options.error(e.error)
36047
+ });
36048
+ }
36049
+ // ── Not contract, and deliberately so ────────────────────────────────────
36050
+ /**
36051
+ * Write user-profile fields onto the signed-in user.
36052
+ *
36053
+ * Not one of the ten. It is the `Set User Properties` node's whole
36054
+ * implementation, and BCN-006's Desired State §3 makes it schema-driven across
36055
+ * five backends — which is step 6, not this pass. It moves with the wire
36056
+ * because leaving it behind would have left a second `_makeRequest` in
36057
+ * `userservice.ts`, and the entire claim of this move is that there is one.
36058
+ */
36059
+ setUserProperties(handle, options) {
36060
+ const _cu = this.getCurrentUser(handle);
36061
+ if (_cu === void 0) {
36062
+ options.error("Nobody is signed in.");
36063
+ return;
36064
+ }
36065
+ {
36066
+ const propsToSave = this.serializeObject({ ...options.properties }, "_User");
36067
+ const _content = Object.assign(
36068
+ {},
36069
+ { email: options.email, username: options.username },
36070
+ propsToSave
36071
+ );
36072
+ delete _content.emailVerified;
36073
+ delete _content.createdAt;
36074
+ delete _content.updatedAt;
36075
+ this._makeRequest(handle, "/users/" + _cu.objectId, {
36076
+ method: "PUT",
36077
+ content: _content,
36078
+ success: (response) => {
36079
+ Object.assign(_cu, _content);
36080
+ this.setSession(handle, _cu);
36081
+ options.success(response);
36082
+ },
36083
+ error: (e) => {
36084
+ options.error(e.error);
36085
+ }
36086
+ });
36087
+ }
36088
+ }
36089
+ /** The sign-in methods this backend offers — for rendering a set of buttons. */
36090
+ listAuthProviders(handle, options) {
36091
+ this._makeRequest(handle, "/auth/providers", {
36092
+ method: "GET",
36093
+ success: (response) => options.success(response),
36094
+ error: (e) => options.error(e.error)
36095
+ });
36096
+ }
36097
+ // ── BAK-004 — the provider return leg ────────────────────────────────────
36098
+ /**
36099
+ * Look for a sign-in result in the current URL and act on it.
36100
+ *
36101
+ * Returns true when this page load IS a sign-in return, so the caller can skip
36102
+ * its ordinary stale-session validation.
36103
+ *
36104
+ * **Per backend, not contract.** BCN-006 step 5 is four different redirect
36105
+ * shapes — Parse `authData`, Directus SSO, Supabase, PocketBase
36106
+ * `auth-with-oauth2` — and this is the first of the four. What they have in
36107
+ * common is only the *outcome*: a session, established. That is where the
36108
+ * contract's surface is, and it is `signInWithProvider` plus a `loggedIn`.
36109
+ */
36110
+ consumeAuthReturn(handle) {
36111
+ if (typeof window === "undefined" || !window.location) return false;
36112
+ let params;
36113
+ try {
36114
+ params = new URLSearchParams(window.location.search);
36115
+ } catch (e) {
36116
+ return false;
36117
+ }
36118
+ const code = params.get(HANDOFF_PARAM);
36119
+ const error = params.get(AUTH_ERROR_PARAM);
36120
+ if (!code && !error) return false;
36121
+ params.delete(HANDOFF_PARAM);
36122
+ params.delete(AUTH_ERROR_PARAM);
36123
+ this._stripAuthParamsFromUrl(params);
36124
+ if (error) {
36125
+ this.oauthReturn = { inProgress: false, succeeded: false, error };
36126
+ setTimeout(() => this.emitAuthEvent("oauthReturn", this.oauthReturn), 0);
36127
+ return true;
36128
+ }
36129
+ this.oauthReturn = { inProgress: true };
36130
+ this._makeRequest(handle, "/oauth/exchange", {
36131
+ method: "POST",
36132
+ content: { code },
36133
+ success: (response) => {
36134
+ this.setSession(handle, response);
36135
+ this.oauthReturn = {
36136
+ inProgress: false,
36137
+ succeeded: true,
36138
+ outcome: response.authOutcome,
36139
+ notice: response.authNotice || void 0
36140
+ };
36141
+ this.emitAuthEvent("oauthReturn", this.oauthReturn);
36142
+ this.emitAuthEvent("loggedIn", response);
36143
+ },
36144
+ error: (e) => {
36145
+ this.oauthReturn = {
36146
+ inProgress: false,
36147
+ succeeded: false,
36148
+ error: e && e.error || "Sign-in could not be completed."
36149
+ };
36150
+ this.emitAuthEvent("oauthReturn", this.oauthReturn);
36151
+ }
36152
+ });
36153
+ return true;
36154
+ }
36155
+ /** Rewrite the address bar without the sign-in parameters, keeping everything else. */
36156
+ _stripAuthParamsFromUrl(remaining) {
36157
+ if (!window.history || typeof window.history.replaceState !== "function") return;
36158
+ const query2 = remaining.toString();
36159
+ const cleaned = window.location.pathname + (query2 ? `?${query2}` : "") + window.location.hash;
36160
+ try {
36161
+ window.history.replaceState(window.history.state, "", cleaned);
36162
+ } catch (e) {
36163
+ }
36164
+ }
36165
+ _currentUrlWithoutAuthParams() {
36166
+ const url = new URL(window.location.href);
36167
+ url.searchParams.delete(HANDOFF_PARAM);
36168
+ url.searchParams.delete(AUTH_ERROR_PARAM);
36169
+ return url.toString();
36170
+ }
36171
+ };
36172
+ }
36173
+ });
36174
+
35616
36175
  // ../noodl-runtime/src/api/backends/RestAuthAdapter.ts
35617
36176
  function restAuthProfileFor(type) {
35618
36177
  return PROFILES[type];
@@ -35704,6 +36263,7 @@ var init_RestAuthAdapter = __esm({
35704
36263
  "../noodl-runtime/src/api/backends/RestAuthAdapter.ts"() {
35705
36264
  init_src();
35706
36265
  init_AuthEvents();
36266
+ init_ParseAuthAdapter();
35707
36267
  init_SessionStore();
35708
36268
  init_TokenLifecycle();
35709
36269
  DEFAULT_POCKETBASE_USER_COLLECTION = "users";
@@ -36269,12 +36829,20 @@ var init_RestAuthAdapter = __esm({
36269
36829
  { method, path: this.path(profile, profile.me), token, body: method === "POST" ? {} : void 0 },
36270
36830
  {
36271
36831
  ok: (body) => {
36832
+ if (sessionTokenOf(this.getCurrentUser(handle)) !== sessionTokenOf(stored)) {
36833
+ options.error(SESSION_MOVED_MESSAGE);
36834
+ return;
36835
+ }
36272
36836
  const session = this.readSession(profile, body, Object.assign({}, stored, { sessionToken: token })) ?? Object.assign({}, stored, this.readUser(profile, body), { sessionToken: token });
36273
36837
  this.setSession(handle, session);
36274
36838
  this.emitAuthEvent("sessionGained", session);
36275
36839
  options.success(session);
36276
36840
  },
36277
36841
  fail: (error) => {
36842
+ if (sessionTokenOf(this.getCurrentUser(handle)) !== sessionTokenOf(stored)) {
36843
+ options.error(SESSION_MOVED_MESSAGE);
36844
+ return;
36845
+ }
36278
36846
  if (error.status === 401 || error.status === 403) {
36279
36847
  this.sessionStore(handle).clear();
36280
36848
  this.emitAuthEvent("sessionLost", error.error);
@@ -38775,8 +39343,9 @@ function schemaFor(collectionName2) {
38775
39343
  try {
38776
39344
  collection = CloudStore._collections[collectionName2];
38777
39345
  } catch (e) {
38778
- return void 0;
39346
+ collection = void 0;
38779
39347
  }
39348
+ if (!collection) collection = hostCollection(collectionName2);
38780
39349
  if (!collection) return void 0;
38781
39350
  if (collection.schema) return { collection: collectionName2, properties: collection.schema.properties };
38782
39351
  if (Array.isArray(collection.columns)) {
@@ -38790,6 +39359,15 @@ function schemaFor(collectionName2) {
38790
39359
  }
38791
39360
  return void 0;
38792
39361
  }
39362
+ function hostCollection(collectionName2) {
39363
+ if (typeof _noodl_cloud_runtime_version === "undefined" || typeof _noodl_cloudservices === "undefined") return void 0;
39364
+ if (!_noodl_cloudservices || typeof _noodl_cloudservices.collections !== "function") return void 0;
39365
+ try {
39366
+ return (_noodl_cloudservices.collections() || []).find((c) => c && c.name === collectionName2);
39367
+ } catch (e) {
39368
+ return void 0;
39369
+ }
39370
+ }
38793
39371
  function backendType() {
38794
39372
  try {
38795
39373
  const handle = CloudStore.instance && CloudStore.instance._handle && CloudStore.instance._handle();
@@ -70138,7 +70716,7 @@ function syncDataAttributesOnElement(node) {
70138
70716
  node._appliedDataAttributes = Object.keys(want);
70139
70717
  }
70140
70718
  function claimDataAttributeInputs(node) {
70141
- const own = node.registerInputIfNeeded;
70719
+ const own2 = node.registerInputIfNeeded;
70142
70720
  node.registerInputIfNeeded = function(name2) {
70143
70721
  if (!this.hasInput(name2) && DATA_ATTRIBUTE_NAME.test(name2)) {
70144
70722
  this.registerInput(name2, {
@@ -70150,7 +70728,7 @@ function claimDataAttributeInputs(node) {
70150
70728
  });
70151
70729
  return;
70152
70730
  }
70153
- return own ? own.call(this, name2) : void 0;
70731
+ return own2 ? own2.call(this, name2) : void 0;
70154
70732
  };
70155
70733
  }
70156
70734
  function scrollNodeIntoView(node, align) {
@@ -71700,7 +72278,7 @@ var init_Checkbox2 = __esm({
71700
72278
  });
71701
72279
 
71702
72280
  // ../noodl-viewer-react/src/guid.ts
71703
- function guid() {
72281
+ function guid2() {
71704
72282
  function s4() {
71705
72283
  return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
71706
72284
  }
@@ -71709,7 +72287,7 @@ function guid() {
71709
72287
  var guid_default;
71710
72288
  var init_guid = __esm({
71711
72289
  "../noodl-viewer-react/src/guid.ts"() {
71712
- guid_default = guid;
72290
+ guid_default = guid2;
71713
72291
  }
71714
72292
  });
71715
72293
 
@@ -71929,6 +72507,17 @@ var init_checkbox = __esm({
71929
72507
  });
71930
72508
 
71931
72509
  // ../noodl-viewer-react/src/components/controls/Select/Select.tsx
72510
+ function domValueOf(item) {
72511
+ if (item.Value === void 0 || item.Value === null || typeof item.Value === "boolean") {
72512
+ const label = item.Label;
72513
+ return (label === void 0 || label === null || typeof label === "boolean" ? "" : String(label)).replace(/[ \t\n\f\r]+/g, " ").trim();
72514
+ }
72515
+ return String(item.Value);
72516
+ }
72517
+ function offersValue(items, value2) {
72518
+ if (!items || typeof value2 !== "string") return false;
72519
+ return items.some((item) => item !== null && item !== void 0 && domValueOf(item) === value2);
72520
+ }
71932
72521
  function Select(props) {
71933
72522
  const [value2, setValue] = (0, import_react6.useState)(props.value);
71934
72523
  (0, import_react6.useEffect)(() => {
@@ -71942,7 +72531,7 @@ function Select(props) {
71942
72531
  style2 = Object.assign({}, props.textStyle, style2);
71943
72532
  style2.color = props.noodlNode.context.styles.resolveColor(style2.color);
71944
72533
  }
71945
- const selectedIndex = !props.items || value2 === void 0 ? -1 : props.items.findIndex((i2) => i2.Value === value2);
72534
+ const selectedIndex = !props.items || value2 === void 0 ? -1 : props.items.findIndex((i2) => i2 !== null && i2 !== void 0 && domValueOf(i2) === value2);
71946
72535
  const { height, ...otherStyles } = style2;
71947
72536
  function _renderIcon() {
71948
72537
  if (props.iconSourceType === "image" && props.iconImageSource !== void 0)
@@ -72050,7 +72639,7 @@ function Select(props) {
72050
72639
  {
72051
72640
  ...inputProps,
72052
72641
  disabled: !props.enabled,
72053
- value: options.find((i2) => i2.props.value === value2) ? value2 : void 0,
72642
+ value: offersValue(props.items, value2) ? value2 : "",
72054
72643
  ...utils_default.controlEvents(props),
72055
72644
  onChange: (e) => {
72056
72645
  setValue(e.target.value);
@@ -72223,12 +72812,14 @@ var init_options = __esm({
72223
72812
  this.props.id = "input-" + guid_default();
72224
72813
  this.props.items = DEFAULT_ITEMS.map((item) => ({ ...item }));
72225
72814
  this.props.value = this._internal.value = DEFAULT_ITEMS[0].Value;
72815
+ this._internal.valueIsSeed = true;
72226
72816
  this.props.valueWrites = 0;
72227
72817
  this.props.valueChanged = (value2) => {
72228
72818
  const changed = this._internal.value !== value2;
72229
72819
  this.props.value = this._internal.value = value2;
72230
72820
  this.props.valueWrites++;
72231
72821
  if (changed) {
72822
+ this._internal.valueIsSeed = false;
72232
72823
  this.flagOutputDirty("value");
72233
72824
  this.sendSignalOnOutput("onChange");
72234
72825
  }
@@ -72281,6 +72872,16 @@ var init_options = __esm({
72281
72872
  if (newValue && typeof newValue.on === "function") newValue.on("change", this._itemsChanged);
72282
72873
  }
72283
72874
  this.props.items = optionItems(this._internal.items);
72875
+ if (this._internal.valueIsSeed) {
72876
+ const offered = this.props.items;
72877
+ const seed = this._internal.value;
72878
+ if (!Array.isArray(offered) || !offered.some((item) => item && item.Value === seed)) {
72879
+ this._internal.valueIsSeed = false;
72880
+ this.props.value = this._internal.value = "";
72881
+ this.props.valueWrites++;
72882
+ this.flagOutputDirty("value");
72883
+ }
72884
+ }
72284
72885
  this.forceUpdate();
72285
72886
  }
72286
72887
  },
@@ -72299,6 +72900,7 @@ var init_options = __esm({
72299
72900
  }
72300
72901
  const changed = value2 !== this._internal.value;
72301
72902
  this.props.value = this._internal.value = value2;
72903
+ this._internal.valueIsSeed = false;
72302
72904
  this.props.valueWrites++;
72303
72905
  if (changed) {
72304
72906
  this.forceUpdate();
@@ -78605,11 +79207,11 @@ var init_foreach = __esm({
78605
79207
  const internal2 = this._internal;
78606
79208
  const children = internal2.target.getChildren();
78607
79209
  let at = children.indexOf(this) + 1;
78608
- let own = 0;
79210
+ let own2 = 0;
78609
79211
  for (let i2 = at; i2 < children.length; i2++) {
78610
79212
  if (internal2.itemNodes.indexOf(children[i2]) === -1) continue;
78611
- if (own === listIndex) return i2;
78612
- own++;
79213
+ if (own2 === listIndex) return i2;
79214
+ own2++;
78613
79215
  at = i2 + 1;
78614
79216
  }
78615
79217
  return at;
@@ -90999,8 +91601,8 @@ var init_timer = __esm({
90999
91601
  const a = this._internal._animation;
91000
91602
  if (a._isRunning !== true || this._internal.paused) return;
91001
91603
  const left = a.durationLeft();
91002
- const own = this._internal.resumed || { duration: a.duration, delay: a.delay };
91003
- this._internal.paused = { left: typeof left === "number" && left >= 0 ? left : a.duration, ...own };
91604
+ const own2 = this._internal.resumed || { duration: a.duration, delay: a.delay };
91605
+ this._internal.paused = { left: typeof left === "number" && left >= 0 ? left : a.duration, ...own2 };
91004
91606
  a.stop();
91005
91607
  },
91006
91608
  _onPageShown: function() {
@@ -95191,6 +95793,7 @@ __export(cloudfunction2_exports, {
95191
95793
  default: () => cloudfunction2_default
95192
95794
  });
95193
95795
  function _makeRequest(path10, options) {
95796
+ options = holdOptionsForWrite(options);
95194
95797
  const xhr = new XMLHttpRequest();
95195
95798
  xhr.onreadystatechange = function() {
95196
95799
  if (xhr.readyState === 4) {
@@ -95235,6 +95838,7 @@ var init_cloudfunction2 = __esm({
95235
95838
  "../noodl-viewer-react/src/nodes/std-library/data/cloudfunction2.ts"() {
95236
95839
  import_runtime10 = __toESM(require_noodl_runtime());
95237
95840
  init_SessionStore();
95841
+ init_pressguard();
95238
95842
  init_outcome();
95239
95843
  CALL_ERROR_CODE = "cloud-function/call-failed";
95240
95844
  CloudFunctionNode = {
@@ -97873,453 +98477,6 @@ var init_router_navigate = __esm({
97873
98477
  }
97874
98478
  });
97875
98479
 
97876
- // ../noodl-runtime/src/api/backends/ParseAuthAdapter.ts
97877
- function pageSays(response, phrase) {
97878
- return typeof response === "string" && response.indexOf(phrase) !== -1;
97879
- }
97880
- function htmlPageError(error, invalidPhrase, invalidMessage, fallback) {
97881
- if (typeof error !== "string" || error.indexOf("<") === -1) return error || fallback;
97882
- return error.indexOf(invalidPhrase) !== -1 ? invalidMessage : fallback;
97883
- }
97884
- var guid2, HANDOFF_PARAM, AUTH_ERROR_PARAM, ParseAuthAdapter;
97885
- var init_ParseAuthAdapter = __esm({
97886
- "../noodl-runtime/src/api/backends/ParseAuthAdapter.ts"() {
97887
- init_AuthEvents();
97888
- init_SessionStore();
97889
- init_TokenLifecycle();
97890
- guid2 = require_guid();
97891
- HANDOFF_PARAM = "nodegx_auth";
97892
- AUTH_ERROR_PARAM = "nodegx_auth_error";
97893
- ParseAuthAdapter = class extends AuthEvents {
97894
- constructor(options) {
97895
- super();
97896
- /** BAK-004: the state of a sign-in returning from a provider or a magic link. */
97897
- this.oauthReturn = { inProgress: false };
97898
- /**
97899
- * One store and one controller per backend, keyed by application id.
97900
- *
97901
- * Per handle rather than per adapter because the contract's whole point is
97902
- * that a project may have more than one backend, and two backends have two
97903
- * sessions — the phase decided explicitly that they are never shared.
97904
- */
97905
- this.stores = /* @__PURE__ */ new Map();
97906
- this.controllers = /* @__PURE__ */ new Map();
97907
- this.serializeObject = options.serializeObject;
97908
- this.createStore = options.createStore || parseSessionStore;
97909
- this.lifecycle = options.lifecycle || { kind: "eternal" };
97910
- }
97911
- // ── Session storage ──────────────────────────────────────────────────────
97912
- sessionStore(handle) {
97913
- const appId = handle.publicToken;
97914
- const cacheKey = String(appId);
97915
- let store2 = this.stores.get(cacheKey);
97916
- if (store2 === void 0) {
97917
- store2 = this.createStore(appId);
97918
- this.stores.set(cacheKey, store2);
97919
- }
97920
- return store2;
97921
- }
97922
- /**
97923
- * The lifecycle controller for this backend.
97924
- *
97925
- * On Parse this is an `eternal` controller: it arms no timer, subscribes to no
97926
- * storage events, and its gate is a synchronous pass-through. It is on the
97927
- * request path anyway, so that BCN-004 adds a `performRefresh` rather than a
97928
- * call site.
97929
- */
97930
- lifecycleController(handle) {
97931
- const cacheKey = String(handle.publicToken);
97932
- let controller = this.controllers.get(cacheKey);
97933
- if (controller === void 0) {
97934
- controller = new TokenLifecycleController({
97935
- lifecycle: this.lifecycle,
97936
- store: this.sessionStore(handle),
97937
- onSessionLost: (reason) => {
97938
- this.emitAuthEvent("sessionChanged", void 0);
97939
- this.emitAuthEvent("sessionLost", reason);
97940
- },
97941
- onSessionClearedElsewhere: () => {
97942
- this.emitAuthEvent("sessionChanged", void 0);
97943
- this.emitAuthEvent("loggedOut");
97944
- }
97945
- });
97946
- controller.start();
97947
- this.controllers.set(cacheKey, controller);
97948
- }
97949
- return controller;
97950
- }
97951
- /** The stored session, or `undefined`. Not a contract method. */
97952
- getCurrentUser(handle) {
97953
- return this.sessionStore(handle).read();
97954
- }
97955
- setSession(handle, session) {
97956
- this.sessionStore(handle).write(session);
97957
- this.lifecycleController(handle).sessionChanged();
97958
- this.emitAuthEvent("sessionChanged", session);
97959
- }
97960
- clearSession(handle) {
97961
- this.sessionStore(handle).clear();
97962
- this.lifecycleController(handle).sessionChanged();
97963
- this.emitAuthEvent("sessionChanged", void 0);
97964
- }
97965
- // ── The wire ─────────────────────────────────────────────────────────────
97966
- /**
97967
- * The Parse auth seam. Moved from `userservice.ts:198` with one substitution:
97968
- * `this.endpoint`/`this.appId` became `handle.url`/`handle.publicToken`.
97969
- *
97970
- * It is deliberately *not* `ParseWireAdapter._makeRequest`. The two differ in
97971
- * three ways that all matter: this one sends `X-Parse-Installation-Id`, never
97972
- * sends a master key, and hands its error callback Parse's `code` (the `209`
97973
- * the session check turns on). Merging them would be a rewrite in a task
97974
- * whose value is that nothing was rewritten.
97975
- */
97976
- _makeRequest(handle, path10, options) {
97977
- if (!handle.url) {
97978
- if (options.error) {
97979
- options.error({ error: "No active cloud service", status: 0 });
97980
- }
97981
- return;
97982
- }
97983
- const store2 = this.sessionStore(handle);
97984
- this.lifecycleController(handle).withSession((session) => {
97985
- const xhr = new XMLHttpRequest();
97986
- xhr.onreadystatechange = function() {
97987
- if (xhr.readyState === 4) {
97988
- let json2;
97989
- try {
97990
- json2 = JSON.parse(xhr.response);
97991
- } catch (e) {
97992
- }
97993
- if (xhr.status === 200 || xhr.status === 201) {
97994
- options.success(json2 || xhr.response);
97995
- } else options.error(json2 || { error: xhr.responseText, status: xhr.status });
97996
- }
97997
- };
97998
- xhr.open(options.method || "GET", handle.url + path10, true);
97999
- xhr.setRequestHeader("X-Parse-Application-Id", handle.publicToken);
98000
- const iidKey = parseInstallationIdKey(handle.publicToken);
98001
- let _iid = store2.readKey(iidKey);
98002
- if (_iid === void 0) {
98003
- _iid = guid2();
98004
- store2.writeKey(iidKey, _iid);
98005
- }
98006
- xhr.setRequestHeader("X-Parse-Installation-Id", _iid);
98007
- if (options.sessionToken) xhr.setRequestHeader("X-Parse-Session-Token", options.sessionToken);
98008
- else if (session !== void 0) {
98009
- xhr.setRequestHeader("X-Parse-Session-Token", session.sessionToken);
98010
- }
98011
- xhr.setRequestHeader("Content-Type", "application/json");
98012
- xhr.send(JSON.stringify(options.content));
98013
- });
98014
- }
98015
- // ── The ten ──────────────────────────────────────────────────────────────
98016
- logIn(handle, options) {
98017
- this._makeRequest(handle, "/login", {
98018
- method: "POST",
98019
- content: {
98020
- username: options.username,
98021
- password: options.password,
98022
- _method: "GET"
98023
- },
98024
- success: (response) => {
98025
- this.setSession(handle, response);
98026
- options.success(response);
98027
- this.emitAuthEvent("loggedIn", response);
98028
- },
98029
- error: (e) => {
98030
- options.error(e.error);
98031
- }
98032
- });
98033
- }
98034
- logOut(handle, options) {
98035
- this._makeRequest(handle, "/logout", {
98036
- method: "POST",
98037
- content: {},
98038
- success: () => {
98039
- this.clearSession(handle);
98040
- options.success();
98041
- this.emitAuthEvent("loggedOut");
98042
- },
98043
- error: (e) => {
98044
- options.error(e.error);
98045
- }
98046
- });
98047
- }
98048
- signUp(handle, options) {
98049
- const additionalUserProps = options.properties ? this.serializeObject({ ...options.properties }, "_User") : {};
98050
- this._makeRequest(handle, "/users", {
98051
- method: "POST",
98052
- content: Object.assign({}, additionalUserProps, {
98053
- username: options.username,
98054
- password: options.password,
98055
- email: options.email
98056
- }),
98057
- success: (response) => {
98058
- const identity2 = { username: options.username };
98059
- if (options.email !== void 0) identity2.email = options.email;
98060
- const _cu = Object.assign(response, identity2, options.properties);
98061
- if (_cu.emailVerified === void 0) _cu.emailVerified = false;
98062
- this.setSession(handle, _cu);
98063
- options.success(response);
98064
- this.emitAuthEvent("loggedIn", _cu);
98065
- },
98066
- error: (e) => {
98067
- options.error(e.error);
98068
- }
98069
- });
98070
- }
98071
- fetchCurrentUser(handle, options) {
98072
- this._makeRequest(handle, "/users/me", {
98073
- method: "GET",
98074
- sessionToken: options.sessionToken,
98075
- success: (response) => {
98076
- this.setSession(handle, response);
98077
- this.emitAuthEvent("sessionGained", response);
98078
- options.success(response);
98079
- },
98080
- error: (e) => {
98081
- if (e.code === 209) {
98082
- this.sessionStore(handle).clear();
98083
- this.emitAuthEvent("sessionLost");
98084
- }
98085
- options.error(e.error);
98086
- }
98087
- });
98088
- }
98089
- verifyEmail(handle, options) {
98090
- this._makeRequest(
98091
- handle,
98092
- "/apps/" + handle.publicToken + "/verify_email?username=" + options.username + "&token=" + options.token,
98093
- {
98094
- method: "GET",
98095
- // This endpoint answers with an HTML page rather than JSON, so the outcome has to be
98096
- // read out of the page's text.
98097
- success: (response) => {
98098
- if (pageSays(response, "Successfully verified your email")) {
98099
- options.success();
98100
- } else if (pageSays(response, "Invalid Verification Link")) {
98101
- options.error("Invalid verification token");
98102
- } else {
98103
- options.error("Failed to verify email");
98104
- }
98105
- },
98106
- error: (e) => {
98107
- options.error(htmlPageError(e.error, "Invalid Verification Link", "Invalid verification token", "Failed to verify email"));
98108
- }
98109
- }
98110
- );
98111
- }
98112
- sendEmailVerification(handle, options) {
98113
- this._makeRequest(handle, "/verificationEmailRequest", {
98114
- method: "POST",
98115
- content: { email: options.email },
98116
- success: () => {
98117
- options.success();
98118
- },
98119
- error: (e) => {
98120
- options.error(e.error);
98121
- }
98122
- });
98123
- }
98124
- resetPassword(handle, options) {
98125
- this._makeRequest(handle, "/apps/" + handle.publicToken + "/request_password_reset", {
98126
- method: "POST",
98127
- content: {
98128
- username: options.username,
98129
- token: options.token,
98130
- new_password: options.newPassword
98131
- },
98132
- success: (response) => {
98133
- if (pageSays(response, "Password successfully reset") || pageSays(response, "Successfully updated your password")) {
98134
- options.success();
98135
- } else if (pageSays(response, "Invalid Link")) {
98136
- options.error("Invalid or expired reset link");
98137
- } else {
98138
- options.error("Failed to reset password");
98139
- }
98140
- },
98141
- error: (e) => {
98142
- options.error(htmlPageError(e.error, "Invalid Link", "Invalid or expired reset link", "Failed to reset password"));
98143
- }
98144
- });
98145
- }
98146
- requestPasswordReset(handle, options) {
98147
- this._makeRequest(handle, "/requestPasswordReset", {
98148
- method: "POST",
98149
- content: { email: options.email },
98150
- success: () => {
98151
- options.success();
98152
- },
98153
- error: (e) => {
98154
- options.error(e.error);
98155
- }
98156
- });
98157
- }
98158
- /**
98159
- * Send the browser to a provider. This NAVIGATES AWAY: nothing after it runs,
98160
- * and the result arrives on a later page load through {@link consumeAuthReturn}.
98161
- *
98162
- * `redirect` defaults to the current page (minus any leftover sign-in
98163
- * parameters), so the user comes back where they were. The backend refuses
98164
- * any target that is neither same-origin nor on its `redirectAllowList` — if
98165
- * sign-in dead-ends with "origin not allowed", that list is the fix.
98166
- */
98167
- signInWithProvider(handle, options) {
98168
- if (!handle.url) {
98169
- if (options.error) options.error("No active cloud service");
98170
- return;
98171
- }
98172
- if (!options.provider) {
98173
- if (options.error) options.error("Sign In With: no provider was set.");
98174
- return;
98175
- }
98176
- const redirect = options.redirect || this._currentUrlWithoutAuthParams();
98177
- const url = `${handle.url}/oauth/${encodeURIComponent(options.provider)}/start?redirect=${encodeURIComponent(redirect)}`;
98178
- window.location.href = url;
98179
- }
98180
- /**
98181
- * Ask the backend to email a one-click sign-in link.
98182
- *
98183
- * Succeeds identically for a known and an unknown address — the endpoint is
98184
- * anonymous and anti-enumerating by design, so "success" here means "the
98185
- * request was accepted", never "an account exists". The node's help text says
98186
- * so, because a UI that claims "check your inbox" only for real accounts
98187
- * re-creates the oracle the endpoint removed.
98188
- */
98189
- requestMagicLink(handle, options) {
98190
- this._makeRequest(handle, "/auth/magic-link", {
98191
- method: "POST",
98192
- content: { email: options.email, redirect: options.redirect || this._currentUrlWithoutAuthParams() },
98193
- success: () => options.success(),
98194
- error: (e) => options.error(e.error)
98195
- });
98196
- }
98197
- // ── Not contract, and deliberately so ────────────────────────────────────
98198
- /**
98199
- * Write user-profile fields onto the signed-in user.
98200
- *
98201
- * Not one of the ten. It is the `Set User Properties` node's whole
98202
- * implementation, and BCN-006's Desired State §3 makes it schema-driven across
98203
- * five backends — which is step 6, not this pass. It moves with the wire
98204
- * because leaving it behind would have left a second `_makeRequest` in
98205
- * `userservice.ts`, and the entire claim of this move is that there is one.
98206
- */
98207
- setUserProperties(handle, options) {
98208
- const _cu = this.getCurrentUser(handle);
98209
- if (_cu === void 0) {
98210
- options.error("Nobody is signed in.");
98211
- return;
98212
- }
98213
- {
98214
- const propsToSave = this.serializeObject({ ...options.properties }, "_User");
98215
- const _content = Object.assign(
98216
- {},
98217
- { email: options.email, username: options.username },
98218
- propsToSave
98219
- );
98220
- delete _content.emailVerified;
98221
- delete _content.createdAt;
98222
- delete _content.updatedAt;
98223
- this._makeRequest(handle, "/users/" + _cu.objectId, {
98224
- method: "PUT",
98225
- content: _content,
98226
- success: (response) => {
98227
- Object.assign(_cu, _content);
98228
- this.setSession(handle, _cu);
98229
- options.success(response);
98230
- },
98231
- error: (e) => {
98232
- options.error(e.error);
98233
- }
98234
- });
98235
- }
98236
- }
98237
- /** The sign-in methods this backend offers — for rendering a set of buttons. */
98238
- listAuthProviders(handle, options) {
98239
- this._makeRequest(handle, "/auth/providers", {
98240
- method: "GET",
98241
- success: (response) => options.success(response),
98242
- error: (e) => options.error(e.error)
98243
- });
98244
- }
98245
- // ── BAK-004 — the provider return leg ────────────────────────────────────
98246
- /**
98247
- * Look for a sign-in result in the current URL and act on it.
98248
- *
98249
- * Returns true when this page load IS a sign-in return, so the caller can skip
98250
- * its ordinary stale-session validation.
98251
- *
98252
- * **Per backend, not contract.** BCN-006 step 5 is four different redirect
98253
- * shapes — Parse `authData`, Directus SSO, Supabase, PocketBase
98254
- * `auth-with-oauth2` — and this is the first of the four. What they have in
98255
- * common is only the *outcome*: a session, established. That is where the
98256
- * contract's surface is, and it is `signInWithProvider` plus a `loggedIn`.
98257
- */
98258
- consumeAuthReturn(handle) {
98259
- if (typeof window === "undefined" || !window.location) return false;
98260
- let params;
98261
- try {
98262
- params = new URLSearchParams(window.location.search);
98263
- } catch (e) {
98264
- return false;
98265
- }
98266
- const code = params.get(HANDOFF_PARAM);
98267
- const error = params.get(AUTH_ERROR_PARAM);
98268
- if (!code && !error) return false;
98269
- params.delete(HANDOFF_PARAM);
98270
- params.delete(AUTH_ERROR_PARAM);
98271
- this._stripAuthParamsFromUrl(params);
98272
- if (error) {
98273
- this.oauthReturn = { inProgress: false, succeeded: false, error };
98274
- setTimeout(() => this.emitAuthEvent("oauthReturn", this.oauthReturn), 0);
98275
- return true;
98276
- }
98277
- this.oauthReturn = { inProgress: true };
98278
- this._makeRequest(handle, "/oauth/exchange", {
98279
- method: "POST",
98280
- content: { code },
98281
- success: (response) => {
98282
- this.setSession(handle, response);
98283
- this.oauthReturn = {
98284
- inProgress: false,
98285
- succeeded: true,
98286
- outcome: response.authOutcome,
98287
- notice: response.authNotice || void 0
98288
- };
98289
- this.emitAuthEvent("oauthReturn", this.oauthReturn);
98290
- this.emitAuthEvent("loggedIn", response);
98291
- },
98292
- error: (e) => {
98293
- this.oauthReturn = {
98294
- inProgress: false,
98295
- succeeded: false,
98296
- error: e && e.error || "Sign-in could not be completed."
98297
- };
98298
- this.emitAuthEvent("oauthReturn", this.oauthReturn);
98299
- }
98300
- });
98301
- return true;
98302
- }
98303
- /** Rewrite the address bar without the sign-in parameters, keeping everything else. */
98304
- _stripAuthParamsFromUrl(remaining) {
98305
- if (!window.history || typeof window.history.replaceState !== "function") return;
98306
- const query2 = remaining.toString();
98307
- const cleaned = window.location.pathname + (query2 ? `?${query2}` : "") + window.location.hash;
98308
- try {
98309
- window.history.replaceState(window.history.state, "", cleaned);
98310
- } catch (e) {
98311
- }
98312
- }
98313
- _currentUrlWithoutAuthParams() {
98314
- const url = new URL(window.location.href);
98315
- url.searchParams.delete(HANDOFF_PARAM);
98316
- url.searchParams.delete(AUTH_ERROR_PARAM);
98317
- return url.toString();
98318
- }
98319
- };
98320
- }
98321
- });
98322
-
98323
98480
  // ../noodl-viewer-react/src/nodes/std-library/user/userservice.ts
98324
98481
  var import_events2, import_runtime14, import_cloudstore2, UserService, _instance, userservice_default;
98325
98482
  var init_userservice = __esm({
@@ -98865,44 +99022,20 @@ var signup_exports = {};
98865
99022
  __export(signup_exports, {
98866
99023
  default: () => signup_default
98867
99024
  });
98868
- function updatePorts6(nodeId, _parameters, editorConnection, systemCollections) {
98869
- const ports = [];
98870
- if (systemCollections) {
98871
- const c = systemCollections.find((c2) => c2.name === "_User");
98872
- if (c && c.schema && c.schema.properties) {
98873
- const props = c.schema.properties;
98874
- const _ignoreKeys = ["authData", "password", "username", "createdAt", "updatedAt", "emailVerified", "email", "ACL"];
98875
- for (const key in props) {
98876
- if (_ignoreKeys.indexOf(key) !== -1) continue;
98877
- const p = props[key];
98878
- if (ports.find((_p) => _p.name === key)) continue;
98879
- if (p.type === "Relation") {
98880
- } else {
98881
- const _typeMap = {
98882
- String: "string",
98883
- Boolean: "boolean",
98884
- Number: "number",
98885
- Date: "date"
98886
- };
98887
- ports.push({
98888
- type: {
98889
- name: _typeMap[p.type] ? _typeMap[p.type] : "*"
98890
- },
98891
- plug: "input",
98892
- group: "Properties",
98893
- name: "prop-" + key,
98894
- displayName: key
98895
- });
98896
- }
98897
- }
98898
- }
98899
- }
98900
- editorConnection.sendDynamicPorts(nodeId, ports);
99025
+ function updatePorts6(nodeId, editorConnection, graphModel) {
99026
+ const ctx = userSchemaContext(graphModel, {});
99027
+ const ignore = isParseWireContext(ctx) ? USER_INPUT_IGNORE_PARSE_BROWSER : USER_INPUT_IGNORE_REST;
99028
+ sendSchemaPorts(editorConnection, nodeId, userPropertyPorts(ctx, { plug: "input", ignore }), {
99029
+ staticPorts: staticPortNames(SignUpNodeDefinition)
99030
+ });
98901
99031
  }
98902
99032
  var SIGN_UP_ERROR_CODE, SignUpNodeDefinition, SignUpModule, signup_default;
98903
99033
  var init_signup = __esm({
98904
99034
  "../noodl-viewer-react/src/nodes/std-library/user/signup.ts"() {
98905
99035
  "use strict";
99036
+ init_record_ports();
99037
+ init_schema_ports();
99038
+ init_user_ports();
98906
99039
  init_outcome();
98907
99040
  init_userservice();
98908
99041
  SIGN_UP_ERROR_CODE = "user/sign-up-failed";
@@ -99040,23 +99173,11 @@ var init_signup = __esm({
99040
99173
  return;
99041
99174
  }
99042
99175
  function _managePortsForNode(node) {
99043
- updatePorts6(
99044
- node.id,
99045
- node.parameters,
99046
- editorConnection,
99047
- graphModel.getMetaData("systemCollections")
99048
- );
99049
- node.on("parameterUpdated", function() {
99050
- updatePorts6(
99051
- node.id,
99052
- node.parameters,
99053
- editorConnection,
99054
- graphModel.getMetaData("systemCollections")
99055
- );
99056
- });
99057
- graphModel.on("metadataChanged.systemCollections", function(data) {
99058
- updatePorts6(node.id, node.parameters, editorConnection, data);
99059
- });
99176
+ const rebuild = () => updatePorts6(node.id, editorConnection, graphModel);
99177
+ rebuild();
99178
+ graphModel.on("metadataChanged.systemCollections", rebuild);
99179
+ graphModel.on("metadataChanged.dbCollections", rebuild);
99180
+ graphModel.on("metadataChanged.backendServices", rebuild);
99060
99181
  }
99061
99182
  graphModel.on("editorImportComplete", () => {
99062
99183
  graphModel.on("nodeAdded.net.noodl.user.SignUp", function(node) {
@@ -100237,6 +100358,7 @@ __export(cloudfunction_exports, {
100237
100358
  default: () => cloudfunction_default
100238
100359
  });
100239
100360
  function _makeRequest2(path10, options) {
100361
+ options = holdOptionsForWrite(options);
100240
100362
  const xhr = new XMLHttpRequest();
100241
100363
  xhr.onreadystatechange = function() {
100242
100364
  if (xhr.readyState === 4) {
@@ -100265,6 +100387,7 @@ var init_cloudfunction = __esm({
100265
100387
  import_runtime15 = __toESM(require_noodl_runtime());
100266
100388
  import_cloudstore3 = __toESM(require_cloudstore());
100267
100389
  init_SessionStore();
100390
+ init_pressguard();
100268
100391
  CloudFunctionNode2 = {
100269
100392
  name: "Cloud Function",
100270
100393
  category: "Cloud Services",
@@ -167354,11 +167477,12 @@ var node_catalog_default = {
167354
167477
  ],
167355
167478
  items: [
167356
167479
  "nothing handed (undefined): the list it has stays",
167480
+ "the value is still the placed node's seed and the list does not offer it: the list is kept, and the value is cleared to '' and sent",
167357
167481
  "otherwise: the list is kept as handed and drawn at the next paint; the value stays, so a list handed again keeps the pick"
167358
167482
  ],
167359
167483
  value: [
167360
167484
  "null: ignored",
167361
- "the value it already holds, as text: nothing",
167485
+ "the value it already holds, as text: nothing (it is no longer the seed)",
167362
167486
  "a change: kept as text, Value follows, and the list selects it at the next paint if it names an option \u2014 Changed does not fire (only a person's pick fires it)"
167363
167487
  ],
167364
167488
  mounted: [
@@ -179739,7 +179863,7 @@ var node_catalog_default = {
179739
179863
  group: "Request",
179740
179864
  plug: "output",
179741
179865
  type: {
179742
- name: "boolean"
179866
+ name: "string"
179743
179867
  },
179744
179868
  isSignal: false,
179745
179869
  description: "Id of the user the session token resolved to, and blank for an unauthenticated request"
@@ -180965,8 +181089,8 @@ var CatalogIndex = class _CatalogIndex {
180965
181089
  suggestPort(typeName4, plug, unknown, kind) {
180966
181090
  const names2 = this.portNames(typeName4, plug);
180967
181091
  const candidates = kind ? names2.filter((name2) => {
180968
- const own = this.portKind(typeName4, plug, name2);
180969
- return own === void 0 || own === kind;
181092
+ const own2 = this.portKind(typeName4, plug, name2);
181093
+ return own2 === void 0 || own2 === kind;
180970
181094
  }) : names2;
180971
181095
  return nearest(unknown, candidates);
180972
181096
  }
@@ -196603,7 +196727,8 @@ function coerce(type, value2) {
196603
196727
  }
196604
196728
  }
196605
196729
  }
196606
- function applyRequestContract(specs2, body) {
196730
+ function applyRequestContract(specs2, parsed2) {
196731
+ const body = parsed2 !== null && typeof parsed2 === "object" && !Array.isArray(parsed2) ? parsed2 : {};
196607
196732
  const values2 = { ...body };
196608
196733
  const errors = [];
196609
196734
  for (const spec of specs2) {
@@ -196723,7 +196848,21 @@ for (const invocation of invocations) {
196723
196848
  HOSTS3.push({ invocation: invocations[0], cloudServices: { ...CLOUD, runId: "run-1" } });
196724
196849
  var CloudRequest = defineNode({
196725
196850
  type: "noodl.cloud.request",
196726
- version: 1,
196851
+ version: 3,
196852
+ changes: [
196853
+ {
196854
+ version: 2,
196855
+ says: "A body that is not a JSON object, sent to a function whose parameters declare a contract, names no fields: a Required one refuses the request with the 400 naming it, and with none Required the function runs. It threw a TypeError reading a name off null (a 500)",
196856
+ row: "P107-S97-NULLBODY (the Beta sweep s5)",
196857
+ migration: { none: "Nothing a project stores changes: only a caller sending a body that is not an object to a function with a contract gets a different answer" }
196858
+ },
196859
+ {
196860
+ version: 3,
196861
+ says: "User Id is text (it carries the id of the person the session token resolved to). It was declared a flag, so a wire into a Number read the id as NaN and the canvas offered it to a signal input",
196862
+ row: "P107-S97-USERIDBOOLEAN (the Beta sweep s6)",
196863
+ migration: { none: "No saved wire stops connecting: measured 106 wires from User Id in 13,839 project files, 70 into script inputs (any type), 35 into text ports, 1 into a record field, none into a signal" }
196864
+ }
196865
+ ],
196727
196866
  source: "packages/noodl-viewer-cloud/src/nodes/cloud/request.ts; packages/noodl-viewer-cloud/src/nodes/cloud/requestContract.ts; packages/noodl-viewer-cloud/src/nodes/cloud/requestOrigin.ts; packages/noodl-viewer-cloud/src/services/userservice.js",
196728
196867
  needs: ["host", "network", "random"],
196729
196868
  worldPool: {
@@ -196758,7 +196897,7 @@ var CloudRequest = defineNode({
196758
196897
  // :120-128 — its getter, `!!`
196759
196898
  auth: { type: "boolean", from: (s) => !!s.authenticated, displayName: "Authenticated", group: "Request", description: "Whether the request carried a session token that resolved to a user" },
196760
196899
  // :129-137 — declared `boolean`, carries the id
196761
- userId: { type: "boolean", from: (s) => s.authUserId, displayName: "User Id", group: "Request", description: "Id of the user the session token resolved to, and blank for an unauthenticated request" },
196900
+ userId: { type: "string", from: (s) => s.authUserId, displayName: "User Id", group: "Request", description: "Id of the user the session token resolved to, and blank for an unauthenticated request" },
196762
196901
  // :142-151
196763
196902
  origin: { type: "string", from: (s) => s.origin, displayName: "Origin", group: "Request", description: "The address the calling app is served from \u2014 its Origin header, or this backend\u2019s own host when the caller sent none. Blank when neither is known (a workflow step has no caller). Caller-supplied like every header: right for links sent back to whoever called, not a proof of where the request came from." }
196764
196903
  }
@@ -199092,7 +199231,7 @@ var Model27 = defineNode({
199092
199231
  // :250-272
199093
199232
  modelId: (s, value2, i2, w) => {
199094
199233
  let v = value2;
199095
- const own = [];
199234
+ const own2 = [];
199096
199235
  if (w.registry.isRecord(v)) v = v.getId();
199097
199236
  else if (typeof v === "object" && v !== null) {
199098
199237
  const data = v;
@@ -199105,20 +199244,20 @@ var Model27 = defineNode({
199105
199244
  if (key === "id" || before3[key] === data[key]) continue;
199106
199245
  if (s.properties.includes(key)) {
199107
199246
  w.send("prop-" + key, s);
199108
- own.push({ derived: "changed-" + key });
199247
+ own2.push({ derived: "changed-" + key });
199109
199248
  }
199110
- own.push("changed");
199249
+ own2.push("changed");
199111
199250
  }
199112
199251
  }
199113
199252
  }
199114
199253
  const previous = s.modelId;
199115
199254
  if (valueDidChange2(previous, v) && i2["runOnChange-modelId"]) {
199116
199255
  const st = { ...s, modelId: v };
199117
- const pulses = [...own];
199256
+ const pulses = [...own2];
199118
199257
  setModelID(st, w, v, pulses);
199119
199258
  return { says: "a new Id, ticked under Run On Value Change: bound to the object it names now (made if none has it) and Fetched fires \u2014 an empty Id unbinds, silently; an object handed writes its keys onto that object first", set: { modelId: v, bound: st.bound, jobs: st.jobs, storeScheduled: st.storeScheduled }, send: [], pulses };
199120
199259
  }
199121
- return { says: "otherwise (the same Id, or unticked): Id is sent and the binding stays; an object handed still writes its keys onto the object it names", set: { modelId: v }, send: ["id"], pulses: own };
199260
+ return { says: "otherwise (the same Id, or unticked): Id is sent and the binding stays; an object handed still writes its keys onto the object it names", set: { modelId: v }, send: ["id"], pulses: own2 };
199122
199261
  },
199123
199262
  // :274-280 — `set: function () {}`
199124
199263
  properties: () => ({ says: "always: nothing while it runs; each name makes an input, an output and its own Changed", send: [] }),
@@ -199179,9 +199318,9 @@ var Model27 = defineNode({
199179
199318
  const model2 = w.registry.model(st.bound);
199180
199319
  for (const key of Object.keys(st.dirty)) {
199181
199320
  const value2 = st.inputValues[key];
199182
- const own = key.indexOf(".") === -1 && model2.get(key) !== value2;
199321
+ const own2 = key.indexOf(".") === -1 && model2.get(key) !== value2;
199183
199322
  model2.set(key, value2, { resolve: true });
199184
- if (own && i2["runOnChange-object"]) {
199323
+ if (own2 && i2["runOnChange-object"]) {
199185
199324
  if (st.properties.includes(key)) {
199186
199325
  w.send("prop-" + key, st);
199187
199326
  pulses.push({ derived: "changed-" + key });
@@ -199219,8 +199358,8 @@ var Model27 = defineNode({
199219
199358
  change: (s, i2, e) => {
199220
199359
  if (e.kind !== "model" || e.id !== s.bound || !i2["runOnChange-object"]) return { send: [] };
199221
199360
  const key = String(e.name);
199222
- const own = s.properties.includes(key);
199223
- return { sendDerived: own ? ["prop-" + key] : [], emitDerived: own ? ["changed-" + key] : [], emit: ["changed"], send: [] };
199361
+ const own2 = s.properties.includes(key);
199362
+ return { sendDerived: own2 ? ["prop-" + key] : [], emitDerived: own2 ? ["changed-" + key] : [], emit: ["changed"], send: [] };
199224
199363
  }
199225
199364
  }
199226
199365
  }
@@ -201821,7 +201960,9 @@ var Dropdown = defineNode({
201821
201960
  // v8 (P107 R21, P107-S88-OPTIONSCAST): a word in a list on Items is an option, labelled and valued by the word — v7 drew an option with no text and the value ''
201822
201961
  // v9 (NSP-019 R35): a Focus that finds the node off the page shows as its warning until a Focus reaches it, and an Icon Source that is not an icon shows as one until an icon or nothing arrives (`diagnostics`)
201823
201962
  // v10 (NSP-019 R38): a wired Width in a row, or a wired Height in a column, that is a percent shows as its warning (wired-grow.ts)
201824
- version: 10,
201963
+ // v11 (P121-TWO005-DROPDOWNSTALE): Items that do not offer the placed node's seed ("Option 1") clear it to '' and send Value
201964
+ // v12 (BETA-S7-DROPDOWNFIRSTPICK): with the value in no option, the element keeps its hidden empty option selected, so every option is a pick
201965
+ version: 12,
201825
201966
  // NSP-010 — v7 is the spec correcting itself, not the runtime changing
201826
201967
  changes: [
201827
201968
  {
@@ -201841,7 +201982,23 @@ var Dropdown = defineNode({
201841
201982
  }
201842
201983
  },
201843
201984
  focusAndIconSourceChange(9),
201844
- growChange(10)
201985
+ growChange(10),
201986
+ {
201987
+ version: 11,
201988
+ says: `A Dropdown nobody has given a Value or a pick holds the first default option, "Option 1", so a placed node shows a real option. When Items arrive and do not offer it, Value now becomes '' (empty) and is sent, as the list shows its placeholder. Before, Value went on saying "Option 1", an option the list did not have, and a form that checked whether anything was chosen sent it. A Value somebody wrote, or a person's pick, is never changed`,
201989
+ row: "P121-TWO005-DROPDOWNSTALE",
201990
+ migration: {
201991
+ none: "Only a value the screen never showed changes: the placeholder was already drawn, and the community template, which met it, already set Value to '' itself. No stored project changes"
201992
+ }
201993
+ },
201994
+ {
201995
+ version: 12,
201996
+ says: "While the Dropdown shows its placeholder because its value is in no option, choosing the first option now chooses it: Value and Changed. Before, the list underneath had already selected that first option (the browser does when the selected option leaves the list, or when options arrive in an empty one), so choosing it did nothing and the placeholder stayed. Every other option was already a pick",
201997
+ row: "BETA-S7-DROPDOWNFIRSTPICK",
201998
+ migration: {
201999
+ none: "A pick that did nothing now does what the person asked; nothing an app could rely on, and no stored project changes"
202000
+ }
202001
+ }
201845
202002
  ],
201846
202003
  diagnostics: { ...focusDiagnostics(controlFocusNotMounted("dropdown")), ...iconSourceDiagnostics(), ...wiredGrowDiagnostic },
201847
202004
  source: "packages/noodl-viewer-react/src/nodes/controls/options.ts",
@@ -201864,7 +202021,8 @@ var Dropdown = defineNode({
201864
202021
  focusMissed: false,
201865
202022
  value: "Option 1",
201866
202023
  items: DEFAULT_ITEMS2,
201867
- domSelected: ""
202024
+ domSelected: "",
202025
+ seeded: true
201868
202026
  },
201869
202027
  // utils.ts :402-411 `outcomeOutputs({ done, unchanged })` (addFocusActions)
201870
202028
  outcomes: ["done", "unchanged"],
@@ -202086,7 +202244,12 @@ var Dropdown = defineNode({
202086
202244
  },
202087
202245
  // options.ts :158-171 — `undefined` is no opinion; anything else is kept as handed, drawn at the next paint (`painted`); the
202088
202246
  // node's value stays (R14: a list handed again keeps the pick — the runtime's effect took the author's Value back until P109 ISL-031, row D34)
202089
- items: (s, v) => v === void 0 ? { says: "nothing handed (undefined): the list it has stays", send: [] } : { says: "otherwise: the list is kept as handed and drawn at the next paint; the value stays, so a list handed again keeps the pick", set: { items: v }, send: [] },
202247
+ items: (s, v) => {
202248
+ if (v === void 0) return { says: "nothing handed (undefined): the list it has stays", send: [] };
202249
+ if (s.seeded && !offersValue2(v, s.value))
202250
+ return { says: "the value is still the placed node's seed and the list does not offer it: the list is kept, and the value is cleared to '' and sent", set: { items: v, value: "", seeded: false }, send: ["value"] };
202251
+ return { says: "otherwise: the list is kept as handed and drawn at the next paint; the value stays, so a list handed again keeps the pick", set: { items: v }, send: [] };
202252
+ },
202090
202253
  // options.ts :181-200 — `null` has no `toString` and is ignored (:182-184); what is not text is made text; a change sends
202091
202254
  // Value and never Changed, and the selection follows it at the next paint (R14: the last word wins — the runtime's effect
202092
202255
  // keyed on the author's Value React last rendered, so a write back to it after a pick left the pick on the screen, row C71,
@@ -202094,8 +202257,8 @@ var Dropdown = defineNode({
202094
202257
  value: (s, v) => {
202095
202258
  if (v === null) return { says: "null: ignored", send: [] };
202096
202259
  const value2 = v === void 0 ? void 0 : String(v);
202097
- if (value2 === s.value) return { says: "the value it already holds, as text: nothing", send: [] };
202098
- return { says: "a change: kept as text, Value follows, and the list selects it at the next paint if it names an option \u2014 Changed does not fire (only a person's pick fires it)", set: { value: value2 }, send: ["value"] };
202260
+ if (value2 === s.value) return { says: "the value it already holds, as text: nothing (it is no longer the seed)", set: { seeded: false }, send: [] };
202261
+ return { says: "a change: kept as text, Value follows, and the list selects it at the next paint if it names an option \u2014 Changed does not fire (only a person's pick fires it)", set: { value: value2, seeded: false }, send: ["value"] };
202099
202262
  },
202100
202263
  // react-component-node.ts :2310-2320
202101
202264
  mounted: (_s, v) => ({ says: "always: the list is put on the page (Did Mount), or taken off it (Will Unmount), at the next paint", set: { mounted: !!v }, send: [] }),
@@ -202157,7 +202320,7 @@ var Dropdown = defineNode({
202157
202320
  if (!option || option.disabled) return NONE4;
202158
202321
  const focus = p.elementFocused ? NONE4 : focusTaken2();
202159
202322
  if (p.domSelected === picked) return focus;
202160
- return join3(focus, { set: { value: picked, domSelected: picked }, send: ["value"], emit: ["onChange"] });
202323
+ return join3(focus, { set: { value: picked, domSelected: picked, seeded: false }, send: ["value"], emit: ["onChange"] });
202161
202324
  }, "keyboard")
202162
202325
  }
202163
202326
  }
@@ -202176,6 +202339,12 @@ function readStoredItems(stored) {
202176
202339
  function editorItems(items) {
202177
202340
  return typeof items === "string" ? readStoredItems(items) : itemRows(items);
202178
202341
  }
202342
+ function offersValue2(items, value2) {
202343
+ if (!items) return false;
202344
+ const rows = typeof items === "string" ? readStoredItems(items) : Array.isArray(items) ? items : [];
202345
+ const words = rows.some((item) => item === null || item === void 0 || typeof item !== "object");
202346
+ return rows.filter((item) => item !== null && item !== void 0).map((item) => words && typeof item !== "object" ? { Value: String(item) } : item).some((item) => typeof item === "object" && item.Value === value2);
202347
+ }
202179
202348
  function itemRows(source2) {
202180
202349
  if (!Array.isArray(source2)) return [];
202181
202350
  const out = [];
@@ -202221,12 +202390,7 @@ function painted4(s) {
202221
202390
  const value2 = s.value;
202222
202391
  const options = optionsOf(s.items);
202223
202392
  const inList = value2 !== void 0 && options.some((o) => o.value === value2);
202224
- const firstEnabled = options.find((o) => !o.disabled)?.value ?? null;
202225
- let dom = s.domSelected;
202226
- if (fresh) dom = inList ? value2 : "";
202227
- else if (inList) dom = value2;
202228
- else if (dom !== "" && dom !== null && !options.some((o) => o.value === dom)) dom = firstEnabled;
202229
- else if (dom === null) dom = firstEnabled;
202393
+ const dom = inList ? value2 : "";
202230
202394
  if (dom !== s.domSelected) set.domSelected = dom;
202231
202395
  return { set, send: [], emit: emit2 };
202232
202396
  }
@@ -207776,7 +207940,7 @@ function copy(w, src) {
207776
207940
  return hearingRecords(w, () => copyInto(w, src));
207777
207941
  }
207778
207942
  function copyInto(w, src) {
207779
- const own = w.st.own;
207943
+ const own2 = w.st.own;
207780
207944
  const source2 = src || [];
207781
207945
  let empty2 = 0;
207782
207946
  for (let i2 = 0; i2 < source2.length; i2++) if (source2[i2] === null || source2[i2] === void 0) empty2++;
@@ -207792,13 +207956,13 @@ function copyInto(w, src) {
207792
207956
  src = Array.prototype.filter.call(source2, (item) => item !== null && item !== void 0);
207793
207957
  }
207794
207958
  const ops = [];
207795
- const list2 = own;
207959
+ const list2 = own2;
207796
207960
  const add = (a) => void ops.push({ kind: "add", rec: a.item, index: a.index });
207797
207961
  const remove = (a) => void ops.push({ kind: "remove", rec: a.item });
207798
207962
  list2.on("add", add);
207799
207963
  list2.on("remove", remove);
207800
207964
  try {
207801
- own.set(src);
207965
+ own2.set(src);
207802
207966
  } finally {
207803
207967
  list2.off("add", add);
207804
207968
  list2.off("remove", remove);
@@ -207966,11 +208130,11 @@ function addItem(w, rec, _index) {
207966
208130
  }
207967
208131
  function childIndexFor(rows, listIndex) {
207968
208132
  let at = 0;
207969
- let own = 0;
208133
+ let own2 = 0;
207970
208134
  for (let i2 = 0; i2 < rows.length; i2++) {
207971
208135
  if (rows[i2].leaving) continue;
207972
- if (own === listIndex) return i2;
207973
- own += 1;
208136
+ if (own2 === listIndex) return i2;
208137
+ own2 += 1;
207974
208138
  at = i2 + 1;
207975
208139
  }
207976
208140
  return at;
@@ -209331,13 +209495,13 @@ function merge(a, b) {
209331
209495
  function then(s, first2, next2) {
209332
209496
  return merge(first2, next2({ ...s, ...first2.set }));
209333
209497
  }
209334
- function select(s, v, own) {
209498
+ function select(s, v, own2) {
209335
209499
  if (!s.inGroup || s.gValue === v) return NONE14;
209336
209500
  const set = { gValue: v };
209337
209501
  if (!s.onPage) return { set, send: [], emit: [] };
209338
209502
  const c = v === s.value;
209339
209503
  if (c === s.checked) return { set, send: [], emit: [] };
209340
- return { set: { ...set, checked: c }, send: ["checked"], emit: own || !c ? ["onChange"] : [] };
209504
+ return { set: { ...set, checked: c }, send: ["checked"], emit: own2 || !c ? ["onChange"] : [] };
209341
209505
  }
209342
209506
  function painted14(s) {
209343
209507
  const set = { ...s.pageEnabled === s.enabled ? {} : { pageEnabled: s.enabled }, ...s.pageVisible === s.visible ? {} : { pageVisible: s.visible } };
@@ -212670,7 +212834,8 @@ var LogIn = defineNode({
212670
212834
  var SignUp = defineNode({
212671
212835
  type: "net.noodl.user.SignUp",
212672
212836
  // v2 (P107 s92, P107-S84-USERACL ruled R29): no `prop-ACL` input (signup.ts `_ignoreKeys` names ACL)
212673
- version: 2,
212837
+ // v3 (P107-S102-SIGNUPBACKEND): the `prop-` inputs are the active backend's accounts table, not the Parse `_User` always
212838
+ version: 3,
212674
212839
  changes: [
212675
212840
  {
212676
212841
  version: 2,
@@ -212679,6 +212844,14 @@ var SignUp = defineNode({
212679
212844
  migration: {
212680
212845
  none: `Ruled "Hide it, like the Record nodes" (R29). The reach measured on 2026-10-12 was 429 User-family nodes in 58 of Richard's projects and none wired to or from ACL, so no saved wire is dropped`
212681
212846
  }
212847
+ },
212848
+ {
212849
+ version: 3,
212850
+ says: "The property inputs are the columns of the active backend's accounts table (directus_users on Directus, users on PocketBase, none on Supabase), as Set User Properties draws them, less the columns that backend owns. Before, Sign Up always offered the Parse _User's columns, so a Directus app was offered fields its users table does not have. On the Parse wire nothing moves",
212851
+ row: "P107-S102-SIGNUPBACKEND",
212852
+ migration: {
212853
+ none: "Measured in s102: 4 of 721 projects on Richard's machine have a REST backend and none uses Sign Up on one, so no saved wire is dropped. On the Parse wire the ignore list is the same seven names"
212854
+ }
212682
212855
  }
212683
212856
  ],
212684
212857
  source: "packages/noodl-viewer-react/src/nodes/std-library/user/signup.ts; userservice.ts; RestAuthAdapter.ts",
@@ -212706,12 +212879,15 @@ var SignUp = defineNode({
212706
212879
  },
212707
212880
  {
212708
212881
  derived: {
212709
- // NSP-020 s84 — what the editor draws (signup.ts `updatePorts` :199-250, from `systemCollections`): one `prop-` per column
212710
- // of `_User` less the ones a sign-up does not set and every Relation ("cannot be set at sign-up time"), typed by the
212711
- // Parse table. No Backend picker. At play there is no schema: a write registers each (:185-195)
212882
+ // NSP-020 s84, P107-S102-SIGNUPBACKEND — what the editor draws (signup.ts `updatePorts`, user-ports.ts as Set User
212883
+ // Properties): one `prop-` per column of the ACTIVE backend's accounts class less the ones a browser cannot write
212884
+ // (`USER_INPUT_IGNORE_PARSE_BROWSER`; off the Parse wire `USER_INPUT_IGNORE_REST`) and every Relation. No Backend picker,
212885
+ // and a saved `backendId` is not read (the builder gives this type the active backend's schema). At play there is no
212886
+ // schema: a write registers each (:185-195)
212712
212887
  inputs: (_params, context) => {
212713
212888
  const out = {};
212714
- for (const [p, decl] of userFields(context, ["authData", "password", "username", "createdAt", "updatedAt", "emailVerified", "email"], "_User")) out["prop-" + p] = { ...signUpProperty(p), type: recordFieldType(decl) };
212889
+ const ignore = onParseWire(context) ? USER_INPUT_IGNORE_PARSE_BROWSER2 : USER_INPUT_IGNORE_REST2;
212890
+ for (const [p, decl] of userFields(context, ignore)) out["prop-" + p] = { ...signUpProperty(p), ...recordFieldPort(decl) };
212715
212891
  return out;
212716
212892
  },
212717
212893
  needs: ["schema"],
@@ -214244,10 +214420,10 @@ var WebSocketNode = defineNode({
214244
214420
  run5.own = { pending: true };
214245
214421
  const url = (run5.getConnection().cfg.url || "").trim();
214246
214422
  run5.connect(false);
214247
- const own = run5.own.settled;
214423
+ const own2 = run5.own.settled;
214248
214424
  const p = run5.patch();
214249
- if (own) {
214250
- const failed = { ...p, outcome: own.outcome, ...own.error !== void 0 ? { error: own.error } : {}, ...settles ? { settles } : {} };
214425
+ if (own2) {
214426
+ const failed = { ...p, outcome: own2.outcome, ...own2.error !== void 0 ? { error: own2.error } : {}, ...settles ? { settles } : {} };
214251
214427
  if (!url)
214252
214428
  return said(
214253
214429
  "a blank URL: any socket held is closed quietly and the countdown and heartbeat stopped (a Connect still waiting: Unchanged), then it fails at once \u2014 Connection State error, Last Error `URL is required to connect`, Retry Count 0, the Status outputs sent, On Error; Failure (websocket/connect-failed)",
@@ -215290,9 +215466,9 @@ var ServerSentEvents = defineNode({
215290
215466
  const settles = s.pendingConnect ? [{ outcome: "unchanged" }] : void 0;
215291
215467
  run5.s.pendingConnect = false;
215292
215468
  run5.doConnect(true);
215293
- const own = run5.own.settled;
215469
+ const own2 = run5.own.settled;
215294
215470
  const p = run5.patch();
215295
- const patch = own ? { ...p, outcome: own.outcome, ...own.error !== void 0 ? { error: own.error } : {}, ...settles ? { settles } : {} } : { ...p, outcome: "pending", ...settles ? { settles } : {} };
215471
+ const patch = own2 ? { ...p, outcome: own2.outcome, ...own2.error !== void 0 ? { error: own2.error } : {}, ...settles ? { settles } : {} } : { ...p, outcome: "pending", ...settles ? { settles } : {} };
215296
215472
  if (!s.url)
215297
215473
  return said(
215298
215474
  "no URL: a stream already open or retrying is dropped silently (its request abandoned, its timer cancelled; a Connect still waiting: Unchanged), then it fails for good \u2014 Last Error `No URL was set on the stream`, On Error, Connection State error (Connected false, Retry Count 0), On Close; Failure (sse/connect-failed)",
@@ -218623,7 +218799,9 @@ var Expression = defineNode({
218623
218799
  var INPUT_PATTERNS2 = [/Inputs\.([A-Za-z0-9_]+)/g, /Inputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]/g];
218624
218800
  var SIGNAL_PATTERNS2 = [
218625
218801
  /Outputs\.([A-Za-z0-9_]+)\s*\(\s*\)/g,
218626
- /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\(\s*\)/g
218802
+ /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\(\s*\)/g,
218803
+ /Outputs\.([A-Za-z0-9_]+)\.send\s*\(\s*\)/g,
218804
+ /Outputs\s*\[\s*(?:'([^'\n]*)'|"([^"\n]*)")\s*\]\.send\s*\(\s*\)/g
218627
218805
  ];
218628
218806
  var OUTPUT_PATTERNS2 = [/Outputs\.([A-Za-z0-9_]+)/g, /Outputs\s*\[\s*"([^"]*)"\s*\]/g];
218629
218807
  function collect2(source2, patterns, into, skip) {
@@ -218823,7 +219001,18 @@ var schedule3 = { scheduled: true };
218823
219001
  var JavaScriptFunction = defineNode({
218824
219002
  type: "JavaScriptFunction",
218825
219003
  // v2 (P109 ISL-051, s33): Error cleared to blank text and sent by a run that works and by a new Script (row C146)
218826
- version: 2,
219004
+ // v3 (P107-S83-SENDFORM): `Outputs.x.send()` names a signal port, as `Outputs.x()` does
219005
+ version: 3,
219006
+ changes: [
219007
+ {
219008
+ version: 3,
219009
+ says: "Code that fires a signal as `Outputs.done.send()` now makes `done` a signal port, and the call fires it (before, `done` was a value port, the call threw \"Cannot read properties of undefined (reading 'send')\", and the run reported Error and Failure every time)",
219010
+ row: "P107-S83-SENDFORM",
219011
+ migration: {
219012
+ none: "Each before was a throw at every run: no value ever reached a `done` read as a value, so a wire from it carried nothing, and the same wire now carries the signal the code meant"
219013
+ }
219014
+ }
219015
+ ],
218827
219016
  source: "packages/noodl-runtime/src/nodes/std-library/simplejavascript.ts",
218828
219017
  contract: "ports from the code and the written lists; when the code runs (the Script \u2014 only while Run is not wired, P107-C155 \u2014, Run, a changed ticked input; once a frame); Inputs, this and Noodl as the code sees them; publish on change; signal outputs; Success and Done after an await; a throw, sync or async; a compile failure; no script \u2014 FUNCTION_FIXTURES, one per rule",
218829
219018
  state: {
@@ -219834,12 +220023,12 @@ function run4(s, trigger, _world) {
219834
220023
  }
219835
220024
  function blockPorts(ws) {
219836
220025
  const io = detectIO2(typeof ws === "string" ? ws : void 0);
219837
- const own = (list2) => (n) => list2.indexOf(n) === -1;
220026
+ const own2 = (list2) => (n) => list2.indexOf(n) === -1;
219838
220027
  return {
219839
- inputs: io.inputs.map((p) => p.name).filter(own(RESERVED_INPUTS2)),
219840
- signalInputs: io.signalInputs.filter(own(RESERVED_INPUTS2)),
219841
- outputs: io.outputs.map((p) => p.name).filter(own(RESERVED_OUTPUTS4)),
219842
- signalOutputs: io.signalOutputs.filter(own(RESERVED_OUTPUTS4))
220028
+ inputs: io.inputs.map((p) => p.name).filter(own2(RESERVED_INPUTS2)),
220029
+ signalInputs: io.signalInputs.filter(own2(RESERVED_INPUTS2)),
220030
+ outputs: io.outputs.map((p) => p.name).filter(own2(RESERVED_OUTPUTS4)),
220031
+ signalOutputs: io.signalOutputs.filter(own2(RESERVED_OUTPUTS4))
219843
220032
  };
219844
220033
  }
219845
220034
  var typeOf = (ws, plug, name2) => typeOfPort2(detectIO2(typeof ws === "string" ? ws : void 0), plug, name2);
@@ -220123,7 +220312,7 @@ function portContextReader(project, options = {}) {
220123
220312
  if (wired.length) context.wired = wired;
220124
220313
  if (wiredOutputs.length) context.wiredOutputs = wiredOutputs;
220125
220314
  if (backends.length) context.backends = backends.map((b) => ({ ...b }));
220126
- const schema = schemaFor3(project.metadata, node.parameters ?? {});
220315
+ const schema = schemaFor3(project.metadata, NO_BACKEND_INPUT.includes(node.type) ? {} : node.parameters ?? {});
220127
220316
  if (schema) context.schema = schema;
220128
220317
  if (stacks.length) context.stacks = stacks;
220129
220318
  if (routers.length) context.routers = routers;
@@ -220132,6 +220321,7 @@ function portContextReader(project, options = {}) {
220132
220321
  return context;
220133
220322
  };
220134
220323
  }
220324
+ var NO_BACKEND_INPUT = ["net.noodl.user.SignUp"];
220135
220325
  function unique(names2) {
220136
220326
  return [...new Set(names2)];
220137
220327
  }
@@ -231753,6 +231943,12 @@ var fs7 = __toESM(require("fs"));
231753
231943
  var path8 = __toESM(require("path"));
231754
231944
 
231755
231945
  // src/projectExport.ts
231946
+ var LOOPBACK = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?(\/|$)/i;
231947
+ function loopbackBackendWarning(metadata) {
231948
+ const endpoint = metadata?.cloudservices?.endpoint;
231949
+ if (typeof endpoint !== "string" || !LOOPBACK.test(endpoint)) return void 0;
231950
+ return `This app talks to a backend on this computer (${endpoint}), and the deployed copy will ask each visitor's own computer for it, so its records, sign-in and functions will not work for anyone else. Put the backend on a server first (deploy_to_vm, or nodegx-deploy --full-stack, ships both), or point the project at a backend others can reach, then deploy again.`;
231951
+ }
231756
231952
  function resolveRootNode(project) {
231757
231953
  const components = project.getComponents();
231758
231954
  const byName = (name2) => components.find((c) => c.name === name2);
@@ -231786,7 +231982,18 @@ function projectModelFromLegacy(legacy) {
231786
231982
  var fs6 = __toESM(require("fs"));
231787
231983
  var path7 = __toESM(require("path"));
231788
231984
  init_scriptfiles();
231789
- var readJson = (file) => JSON.parse(fs6.readFileSync(file, "utf8"));
231985
+ var readJson = (file) => {
231986
+ const text3 = fs6.readFileSync(file, "utf8");
231987
+ try {
231988
+ return JSON.parse(text3);
231989
+ } catch (e) {
231990
+ const why = text3.trim() === "" ? "the file is empty, so it is not valid JSON" : e instanceof Error ? e.message : String(e);
231991
+ throw Object.assign(new Error(`${file.split(path7.sep).join("/")}: ${why}`), { projectFile: file });
231992
+ }
231993
+ };
231994
+ function isProjectFileError(error) {
231995
+ return !!error && typeof error.projectFile === "string";
231996
+ }
231790
231997
  function resolveTarget(target) {
231791
231998
  const abs = path7.resolve(target);
231792
231999
  if (!fs6.existsSync(abs)) throw new Error(`No such file or directory: ${abs}`);
@@ -231916,6 +232123,9 @@ function flatten3(roots) {
231916
232123
  for (const root2 of roots) visit(root2, void 0);
231917
232124
  return out;
231918
232125
  }
232126
+ function recordedVisualRoots(list2) {
232127
+ return Array.isArray(list2) && list2.length > 0 ? { visualRoots: list2.filter((id) => typeof id === "string") } : {};
232128
+ }
231919
232129
  function fromLegacyProject(project) {
231920
232130
  const components = [];
231921
232131
  for (const comp of project.components ?? []) {
@@ -231929,7 +232139,8 @@ function fromLegacyProject(project) {
231929
232139
  components.push({
231930
232140
  name: comp.name,
231931
232141
  nodes: flatten3(graph.roots ?? []),
231932
- connections
232142
+ connections,
232143
+ ...recordedVisualRoots(graph.visualRoots)
231933
232144
  });
231934
232145
  }
231935
232146
  return {
@@ -232677,6 +232888,7 @@ function cloudPortMeaning(type, plug) {
232677
232888
 
232678
232889
  // ../noodl-editor/src/editor/src/validation/functionScriptUses.ts
232679
232890
  init_scriptfiles();
232891
+ var FUNCTION_NODE_TYPE = "JavaScriptFunction";
232680
232892
 
232681
232893
  // ../noodl-editor/src/editor/src/validation/functionPorts.ts
232682
232894
  var SCRIPT_NODE_API_MEMBERS = [
@@ -232695,6 +232907,10 @@ var SCRIPT_DECLARATION = new RegExp(
232695
232907
  )
232696
232908
  );
232697
232909
 
232910
+ // ../noodl-editor/src/editor/src/validation/publicWriteDoor.ts
232911
+ var RECORD_WRITE_SCRIPT_MEMBERS = ["create", "save", "delete", "increment", "addRelation", "removeRelation"];
232912
+ var SCRIPT_WRITE_CALL = new RegExp(`\\bRecords\\s*\\.\\s*(${RECORD_WRITE_SCRIPT_MEMBERS.join("|")})\\s*\\(`);
232913
+
232698
232914
  // ../noodl-editor/src/editor/src/validation/typedWordNotTranslated.ts
232699
232915
  var HAS_LETTER = new RegExp("\\p{L}", "u");
232700
232916
 
@@ -232891,7 +233107,11 @@ var AUTHORED_BLOCKING_WARNINGS = /* @__PURE__ */ new Set([
232891
233107
  // and **no `completed` wires at all**. `completed` is a rare port — authors
232892
233108
  // reach for `done` — so the blast radius of making this blocking is small, and
232893
233109
  // that is a measurement rather than a hope.
232894
- "completed-commits-unchecked" /* CompletedCommitsUnchecked */
233110
+ "completed-commits-unchecked" /* CompletedCommitsUnchecked */,
233111
+ // Bug P121-TWO003-SCRIPTCOMPILE — the runtime's own compile fails, so every Run fails. No benign
233112
+ // reading for a graph an agent just wrote; the write said nothing before, and the first person
233113
+ // to run the function got "That could not be done."
233114
+ "function-script-not-compiled" /* FunctionScriptNotCompiled */
232895
233115
  ]);
232896
233116
 
232897
233117
  // ../noodl-editor/src/editor/src/validation/diagnosticExamples.ts
@@ -233020,6 +233240,17 @@ var DIAGNOSTIC_EXAMPLES = [
233020
233240
  code: "function-trusts-a-field-the-caller-wrote" /* FunctionTrustsAFieldTheCallerWrote */,
233021
233241
  examples: ["cloud-share-my-records-with-someone"],
233022
233242
  why: "a cloud function that returns the records of the shares' owners, never a field the page wrote"
233243
+ },
233244
+ // P112 OWN-026 FIX-06 B — a group joined only through its owner's invitation, written only by a function.
233245
+ {
233246
+ code: "role-named-by-a-field-the-caller-wrote" /* RoleNamedByAFieldTheCallerWrote */,
233247
+ examples: ["cloud-members-can-edit-my-records"],
233248
+ why: "a cloud function that names the role from the invitation's owner, so an invitation only lets someone into its creator's group"
233249
+ },
233250
+ {
233251
+ code: "function-guards-an-open-collection" /* FunctionGuardsAnOpenCollection */,
233252
+ examples: ["cloud-members-can-edit-my-records"],
233253
+ why: `the records only a function creates, with the collection's create rule "nobody": the function runs as the system`
233023
233254
  }
233024
233255
  ];
233025
233256
 
@@ -233210,6 +233441,9 @@ var unknownNodeType = {
233210
233441
  }
233211
233442
  };
233212
233443
 
233444
+ // ../noodl-editor/src/editor/src/validation/rules/nonexistentPort.ts
233445
+ init_renamed_ports();
233446
+
233213
233447
  // ../noodl-editor/src/editor/src/views/panels/propertyeditor/propertyPanelTiers.ts
233214
233448
  var ADVANCED_CSS_GROUPS = {
233215
233449
  // ── plumbing ────────────────────────────────────────────────────────────────
@@ -233288,18 +233522,23 @@ function otherEndKind(catalog, nodeById, conn, plug) {
233288
233522
  }
233289
233523
  return portKindOfTypeName(other.instancePortTypes?.[port5]);
233290
233524
  }
233525
+ function renamedPort(type, port5, plug) {
233526
+ const to = plug === "output" ? renamedOutput(type, port5) : void 0;
233527
+ return to ? { to, sentence: ` It was renamed "${to}" in August 2026: wire "${to}".` } : void 0;
233528
+ }
233291
233529
  function missingDrawnPort(catalog, nodeById, conn, component, node, port5, plug, drawn3) {
233292
233530
  const names2 = [...drawn3];
233293
233531
  const tail = (name2) => name2.slice(name2.indexOf("-") + 1).toLowerCase();
233294
233532
  const suffixed = names2.filter((name2) => tail(name2) === port5.toLowerCase() || tail(name2) === tail(port5));
233295
- const suggestion = suffixed.length > 0 ? nearest(port5, suffixed) ?? suffixed[0] : nearest(port5, names2);
233533
+ const renamed = renamedPort(node.type, port5, plug);
233534
+ const suggestion = renamed && drawn3.has(renamed.to) ? renamed.to : suffixed.length > 0 ? nearest(port5, suffixed) ?? suffixed[0] : nearest(port5, names2);
233296
233535
  const wireKind = otherEndKind(catalog, nodeById, conn, plug);
233297
233536
  const fromProject = names2.filter((name2) => !catalog.hasPort(node.type, plug, name2)).sort();
233298
233537
  const alternatives = [.../* @__PURE__ */ new Set([...fromProject, ...availableAlternatives(catalog, node.type, plug, wireKind).filter((name2) => drawn3.has(name2))])].slice(0, MAX_ALTERNATIVES3);
233299
233538
  return {
233300
233539
  code: "nonexistent-port" /* NonexistentPort */,
233301
233540
  severity: "error",
233302
- message: `${node.type} has no ${plug} named "${port5}" with its settings in this project.`,
233541
+ message: `${node.type} has no ${plug} named "${port5}" with its settings in this project.` + (renamed && drawn3.has(renamed.to) ? renamed.sentence : ""),
233303
233542
  location: {
233304
233543
  component,
233305
233544
  nodeId: node.id,
@@ -233379,12 +233618,13 @@ var nonexistentPort = {
233379
233618
  }
233380
233619
  const wireKind = otherEndKind(catalog, nodeById, conn, plug);
233381
233620
  const prefixed = missingPrefix ? missingPrefix + port5 : void 0;
233382
- const suggestion = catalog.suggestPort(node.type, plug, port5, wireKind) ?? prefixed;
233621
+ const renamed = renamedPort(node.type, port5, plug);
233622
+ const suggestion = renamed?.to ?? catalog.suggestPort(node.type, plug, port5, wireKind) ?? prefixed;
233383
233623
  const alternatives = availableAlternatives(catalog, node.type, plug, wireKind);
233384
233624
  out.push({
233385
233625
  code: "nonexistent-port" /* NonexistentPort */,
233386
233626
  severity: "error",
233387
- message: `${node.type} has no ${plug} named "${port5}".` + (prefixed ? ` Its other ${plug}s exist only as "${missingPrefix}<name>", so a name without the prefix is never made and nothing goes through this wire. If "${port5}" is ${cloudPortMeaning(node.type, plug)}, wire "${prefixed}".` : ""),
233627
+ message: `${node.type} has no ${plug} named "${port5}".` + (renamed?.sentence ?? "") + (prefixed ? ` Its other ${plug}s exist only as "${missingPrefix}<name>", so a name without the prefix is never made and nothing goes through this wire. If "${port5}" is ${cloudPortMeaning(node.type, plug)}, wire "${prefixed}".` : ""),
233388
233628
  location: {
233389
233629
  component: component.name,
233390
233630
  nodeId: node.id,
@@ -233587,6 +233827,7 @@ var detachedPageContent = {
233587
233827
  };
233588
233828
 
233589
233829
  // ../noodl-editor/src/editor/src/validation/rules/invalidHierarchy.ts
233830
+ var RECORDED_VISUAL_ROOT = { allowAsChild: true, category: "Visual" };
233590
233831
  function hierarchyTyper(ctx) {
233591
233832
  const byName = /* @__PURE__ */ new Map();
233592
233833
  for (const c of ctx.project.components) {
@@ -233606,8 +233847,9 @@ function hierarchyTyper(ctx) {
233606
233847
  const childIds = new Set(component.nodes.flatMap((n) => n.children));
233607
233848
  const roots = component.nodes.filter((n) => n.parent === void 0 && !childIds.has(n.id));
233608
233849
  const known = (n) => typeOf2(n.type);
233850
+ const rootType = (n) => known(n) ?? (component.visualRoots?.includes(n.id) ? RECORDED_VISUAL_ROOT : void 0);
233609
233851
  const derived = componentHierarchyType(
233610
- roots.map(known).filter((t) => !!t),
233852
+ roots.map(rootType).filter((t) => !!t),
233611
233853
  component.nodes.map(known).filter((t) => !!t)
233612
233854
  );
233613
233855
  inFlight.delete(typeName4);
@@ -234478,6 +234720,123 @@ var settingsDiagnostic = {
234478
234720
  }
234479
234721
  };
234480
234722
 
234723
+ // ../noodl-editor/src/editor/src/validation/rules/functionScriptNotCompiled.ts
234724
+ var CODE_PREFIX3 = "const Script = (typeof Node !== 'undefined')?Node:undefined;\n";
234725
+ var PARAMS2 = ["Inputs", "Outputs", "Noodl", "Component", "Scripts"];
234726
+ function functionScriptCompileError(script2) {
234727
+ try {
234728
+ const AsyncFunction = Object.getPrototypeOf(async function() {
234729
+ }).constructor;
234730
+ new AsyncFunction(...PARAMS2, CODE_PREFIX3 + script2);
234731
+ return void 0;
234732
+ } catch (e) {
234733
+ const message = e instanceof Error ? e.message : String(e);
234734
+ const line = syntaxErrorLine(script2);
234735
+ return line === void 0 ? { message } : { message, line };
234736
+ }
234737
+ }
234738
+ function syntaxErrorLine(script2) {
234739
+ const file = "nodegx-function-script";
234740
+ try {
234741
+ const vm = typeof require === "function" ? require("vm") : void 0;
234742
+ if (!vm?.Script) return void 0;
234743
+ new vm.Script(`(async function (${PARAMS2.join(", ")}) {
234744
+ ${script2}
234745
+ })`, { filename: file });
234746
+ } catch (e) {
234747
+ const stack = e && typeof e === "object" ? e.stack : void 0;
234748
+ const match = typeof stack === "string" ? new RegExp(`${file}:(\\d+)`).exec(stack) : null;
234749
+ if (match) return Math.max(1, Number(match[1]) - 1);
234750
+ }
234751
+ return void 0;
234752
+ }
234753
+ var functionScriptNotCompiled = {
234754
+ code: "function-script-not-compiled" /* FunctionScriptNotCompiled */,
234755
+ description: "A Function's script does not compile, so the Function can never run.",
234756
+ defaultEnabled: true,
234757
+ run(ctx) {
234758
+ const out = [];
234759
+ for (const { component } of ctx.components) {
234760
+ for (const node of component.nodes) {
234761
+ if (node.type !== FUNCTION_NODE_TYPE) continue;
234762
+ const script2 = node.parameters?.functionScript;
234763
+ if (typeof script2 !== "string" || script2.trim() === "") continue;
234764
+ const error = functionScriptCompileError(script2);
234765
+ if (!error) continue;
234766
+ const name2 = node.label ? `"${node.label}"` : "This Function";
234767
+ out.push({
234768
+ code: "function-script-not-compiled" /* FunctionScriptNotCompiled */,
234769
+ severity: "warning",
234770
+ message: `${name2}'s script does not compile${error.line !== void 0 ? ` (line ${error.line})` : ""}: ${error.message}. It can never run: every Run fails with function/script-not-compiled. Fix the script; a regular expression or URL written inside a template literal loses its backslashes, so \`\\/\\/\` arrives as \`//\`, which starts a comment.`,
234771
+ location: {
234772
+ component: component.name,
234773
+ nodeId: node.id,
234774
+ nodeType: node.type,
234775
+ nodeLabel: node.label,
234776
+ port: "functionScript"
234777
+ }
234778
+ });
234779
+ }
234780
+ }
234781
+ return out;
234782
+ }
234783
+ };
234784
+
234785
+ // ../noodl-editor/src/editor/src/validation/rules/functionNeverReruns.ts
234786
+ var RUN_ON_CHANGE_PREFIX3 = "runOnChange-";
234787
+ var functionNeverReruns = {
234788
+ code: "function-never-reruns" /* FunctionNeverReruns */,
234789
+ description: "Nothing re-runs a node when its inputs change: its control signal is unwired and every wired input is set not to run on change.",
234790
+ defaultEnabled: true,
234791
+ run(ctx) {
234792
+ const out = [];
234793
+ const { catalog } = ctx;
234794
+ for (const { component } of ctx.components) {
234795
+ const incoming = /* @__PURE__ */ new Map();
234796
+ for (const c of component.connections) {
234797
+ const list2 = incoming.get(c.toId);
234798
+ if (list2) list2.push(c);
234799
+ else incoming.set(c.toId, [c]);
234800
+ }
234801
+ for (const node of component.nodes) {
234802
+ const isFunction2 = node.type === FUNCTION_NODE_TYPE;
234803
+ if (!isFunction2 && !catalog.isRunOnValueChangeFamily(node.type)) continue;
234804
+ const inbound = incoming.get(node.id) ?? [];
234805
+ if (inbound.length === 0) continue;
234806
+ if (inbound.some((c) => catalog.getPort(node.type, "input", c.toProperty)?.isSignal)) continue;
234807
+ const governed = [
234808
+ ...new Set(
234809
+ inbound.map((c) => c.toProperty).filter(
234810
+ (port5) => isFunction2 ? port5.startsWith("in-") : !!catalog.getPort(node.type, "input", RUN_ON_CHANGE_PREFIX3 + port5)
234811
+ )
234812
+ )
234813
+ ];
234814
+ if (governed.length === 0) continue;
234815
+ const params = node.parameters ?? {};
234816
+ if (!governed.every((port5) => params[RUN_ON_CHANGE_PREFIX3 + port5] === false)) continue;
234817
+ const name2 = node.label ? `"${node.label}"` : isFunction2 ? "This Function" : "This node";
234818
+ const shown = governed.map((p) => `"${isFunction2 ? p.slice("in-".length) : p}"`).join(", ");
234819
+ out.push({
234820
+ code: "function-never-reruns" /* FunctionNeverReruns */,
234821
+ severity: "warning",
234822
+ message: `Nothing re-runs ${name2} when its inputs change: Run is not wired, and every wired input (${shown}) has Run On Value Change unticked. ` + (isFunction2 ? "It runs once when it loads, and a new value on any of them never runs it again. " : "") + "Wire Run to what should start it, or tick Run On Value Change on the input whose new value should run it.",
234823
+ location: {
234824
+ component: component.name,
234825
+ nodeId: node.id,
234826
+ nodeType: node.type,
234827
+ nodeLabel: node.label,
234828
+ port: RUN_ON_CHANGE_PREFIX3 + governed[0],
234829
+ plug: "input"
234830
+ },
234831
+ suggestion: `${RUN_ON_CHANGE_PREFIX3}${governed[0]}: true`,
234832
+ alternatives: governed.map((p) => `${RUN_ON_CHANGE_PREFIX3}${p}: true`)
234833
+ });
234834
+ }
234835
+ }
234836
+ return out;
234837
+ }
234838
+ };
234839
+
234481
234840
  // ../noodl-editor/src/editor/src/validation/rules/index.ts
234482
234841
  var ALL_RULES = [
234483
234842
  duplicateNodeId,
@@ -234529,6 +234888,11 @@ var ALL_RULES = [
234529
234888
  // the first fires on `prep` and never on the `RunTasks` carrying the defect,
234530
234889
  // so this covers a hole in it rather than repeating it.
234531
234890
  completedCommitsUnchecked,
234891
+ // P121 bugs SCRIPTCOMPILE / NEVERRUNS — a Function that can never run, and one that never
234892
+ // re-runs: breakage of the graph's behaviour, so beside the outcome rules and before the
234893
+ // arrangement ones.
234894
+ functionScriptNotCompiled,
234895
+ functionNeverReruns,
234532
234896
  repeatedSiblingSubtree,
234533
234897
  // DEF-025 — beside `repeatedSiblingSubtree` because both read the visual
234534
234898
  // tree's arrangement rather than wires: that one asks whether siblings
@@ -235223,6 +235587,35 @@ function describeWireHealth(reading) {
235223
235587
  return lines;
235224
235588
  }
235225
235589
 
235590
+ // src/skeleton.ts
235591
+ var SKELETON_PLACEHOLDER_MARKER = " \u2014 nothing built yet. Review the plan in docs/ and start when you are ready.";
235592
+ function untouchedSkeletonPages(project) {
235593
+ const pages = [];
235594
+ for (const component of project.getComponents()) {
235595
+ const nodes = [];
235596
+ component.graph.forEachNode((node) => {
235597
+ nodes.push({ typename: node.typename, parameters: node.parameters ?? {} });
235598
+ });
235599
+ if (!nodes.some((n) => n.typename === "Page")) continue;
235600
+ if (!isUntouchedSkeletonPage(nodes)) return [];
235601
+ pages.push(component.name.replace(/^\/(#__page__\/)?/, ""));
235602
+ }
235603
+ return pages;
235604
+ }
235605
+ function isUntouchedSkeletonPage(nodes) {
235606
+ if (nodes.length !== 2) return false;
235607
+ const page = nodes.find((n) => n.typename === "Page");
235608
+ const text3 = nodes.find((n) => n.typename === "Text");
235609
+ if (!page || !text3) return false;
235610
+ const value2 = text3.parameters["text"];
235611
+ return typeof value2 === "string" && value2.endsWith(SKELETON_PLACEHOLDER_MARKER);
235612
+ }
235613
+ function skeletonWarning(pages) {
235614
+ if (!pages.length) return void 0;
235615
+ const named = pages.length === 1 ? `${pages[0]} is` : `${pages.join(", ")} are`;
235616
+ return `Nothing is built in this project yet: ${named} still the placeholder create_project wrote, whose sentence is addressed to the agent, and it is what every visitor will see. It was published as it is; build the app, then deploy again.`;
235617
+ }
235618
+
235226
235619
  // src/deploy.ts
235227
235620
  var DEPLOY_INDEX = path9.join(EXTERNAL_DIR, "deploy", "index.json");
235228
235621
  function assertDeployRuntime() {
@@ -235294,6 +235687,11 @@ async function deployProject(options) {
235294
235687
  } finally {
235295
235688
  if (stage) fs9.rmSync(stage.stage, { recursive: true, force: true });
235296
235689
  }
235690
+ const cloudservices = project.getMetaData("cloudservices");
235691
+ const loopback = fullStack ? void 0 : loopbackBackendWarning({ cloudservices });
235692
+ const localBackend = loopback ? { endpoint: String(cloudservices?.endpoint), warning: loopback } : void 0;
235693
+ const skeletonPages = untouchedSkeletonPages(project);
235694
+ const skeleton = skeletonWarning(skeletonPages);
235297
235695
  const roots = readDeployedRoots(fullStack ? path9.join(options.outDir, "app") : options.outDir);
235298
235696
  return {
235299
235697
  outDir: options.outDir,
@@ -235312,10 +235710,16 @@ async function deployProject(options) {
235312
235710
  blank: gradeRoots(roots),
235313
235711
  engine,
235314
235712
  wires,
235713
+ ...localBackend ? { localBackend: localBackend.endpoint } : {},
235714
+ ...skeleton ? { skeletonPages } : {},
235315
235715
  // EXP-017 AC4 — a duplicate the copy step could not leave out reaches the person through the
235316
235716
  // same channel every other deploy warning does, rather than only the editor's console.
235317
235717
  warnings: [
235318
235718
  ...warnings,
235719
+ // P112-OWN023-DEPLOYLOOPBACK — a static site cannot reach a backend on the builder's machine. A full-stack
235720
+ // folder carries its backend, and the backend serves the app with its own address in place of this one.
235721
+ ...localBackend ? [localBackend.warning] : [],
235722
+ ...skeleton ? [skeleton] : [],
235319
235723
  ...describeWireHealth(wires),
235320
235724
  ...copyReport.duplicatesKept.map(
235321
235725
  (kept) => `${kept.path} (${kept.bytes} B) is the same file as ${kept.keep} and both were published \u2014 ${kept.reason}. Point that reference at ${kept.keep} to stop shipping it twice.`
@@ -235380,9 +235784,24 @@ async function main() {
235380
235784
  result2 = { ok: true, ...outcome };
235381
235785
  } catch (error) {
235382
235786
  const message = error instanceof Error ? error.message : String(error);
235383
- result2 = error instanceof DevelopmentEngineError ? { ok: false, stage: "engine", message, engine: error.engine } : error instanceof FullStackTargetError ? { ok: false, stage: "target", message } : error instanceof FullStackInstallError ? { ok: false, stage: "runtime", message } : { ok: false, stage: classify(message), message };
235787
+ result2 = error instanceof DevelopmentEngineError ? { ok: false, stage: "engine", message, engine: error.engine } : error instanceof FullStackTargetError ? { ok: false, stage: "target", message } : error instanceof FullStackInstallError ? { ok: false, stage: "runtime", message } : (
235788
+ // P112-S13-DEPLOYBADJSON: a project file that is not JSON was filed under `write`.
235789
+ isProjectFileError(error) ? { ok: false, stage: "project", message } : { ok: false, stage: classify(message), message }
235790
+ );
235384
235791
  }
235385
235792
  realWrite(JSON.stringify(result2) + "\n");
235793
+ if (result2.ok && result2.localBackend) {
235794
+ const sentence = result2.warnings.find((w) => w.includes(result2.localBackend));
235795
+ if (sentence) process.stderr.write(`
235796
+ nodegx-deploy: ${sentence}
235797
+ `);
235798
+ }
235799
+ if (result2.ok && result2.skeletonPages) {
235800
+ const sentence = result2.warnings.find((w) => w.startsWith("Nothing is built in this project yet"));
235801
+ if (sentence) process.stderr.write(`
235802
+ nodegx-deploy: ${sentence}
235803
+ `);
235804
+ }
235386
235805
  process.exitCode = 0;
235387
235806
  }
235388
235807
  main().catch((error) => {