ignotum 0.0.12 → 0.0.14

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.
Files changed (60) hide show
  1. package/dist/cli/bin.mjs +1183 -447
  2. package/dist/cli/bin.mjs.map +1 -1
  3. package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
  4. package/dist/runtime/api-CAgKDij7.js.map +1 -0
  5. package/dist/runtime/{api-5XSrIeqW.d.ts → api-Cv3hMbzo.d.ts} +4 -4
  6. package/dist/runtime/client.d.ts +39 -9
  7. package/dist/runtime/client.js +362 -63
  8. package/dist/runtime/client.js.map +1 -1
  9. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js +298 -0
  10. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +1 -0
  11. package/dist/runtime/id-Bt9XWRGL.js +423 -0
  12. package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
  13. package/dist/runtime/{id-Cs82tq9Q-CK-maMgN.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +2 -2
  14. package/dist/runtime/id-E7JKqQ9r.d.ts +1 -0
  15. package/dist/runtime/index-DWU-7TIh.d.ts +185 -0
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +28 -7
  19. package/dist/runtime/internal/host.js +37 -8
  20. package/dist/runtime/internal/host.js.map +1 -1
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +2 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{pagination-D-R9NR61-CpIoHFoI.d.ts → pagination-CpYCaRYD-C2Rpp4Cw.d.ts} +42 -7
  26. package/dist/runtime/pagination-E7JKqQ9r.d.ts +1 -0
  27. package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
  28. package/dist/runtime/{schema-B9XxyRO8.js → schema-DqogEydG.js} +41 -12
  29. package/dist/runtime/schema-DqogEydG.js.map +1 -0
  30. package/dist/runtime/server.d.ts +7 -4
  31. package/dist/runtime/server.js +2 -2
  32. package/dist/runtime/server.js.map +1 -1
  33. package/dist/runtime/{sync-avN7NkcU.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
  34. package/dist/runtime/types-C1iDP2Yf-BnkfBOQf.d.ts +224 -0
  35. package/package.json +2 -2
  36. package/src/cli/agent-files.ts +10 -0
  37. package/src/cli/build/server.ts +35 -6
  38. package/src/cli/command.ts +25 -2
  39. package/src/cli/deploy.ts +34 -11
  40. package/src/client/hooks.ts +68 -0
  41. package/src/client/id.ts +259 -0
  42. package/src/client/index.ts +5 -2
  43. package/src/client/sync.ts +144 -11
  44. package/src/dev-runtime/database.ts +77 -2
  45. package/src/dev-runtime/functions.ts +111 -88
  46. package/src/dev-runtime/id.ts +190 -0
  47. package/src/dev-runtime/migrations.ts +9 -0
  48. package/src/dev-runtime/query-cache.ts +105 -0
  49. package/src/dev-runtime/sync.ts +149 -17
  50. package/src/internal/host.ts +1 -1
  51. package/src/server/index.ts +2 -0
  52. package/dist/runtime/api-DzcR7spt.js.map +0 -1
  53. package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js +0 -330
  54. package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +0 -1
  55. package/dist/runtime/file-C1abuMgd.js +0 -173
  56. package/dist/runtime/file-C1abuMgd.js.map +0 -1
  57. package/dist/runtime/index-CrWg4Z0y.d.ts +0 -398
  58. package/dist/runtime/pagination-CX5IPbEi.d.ts +0 -1
  59. package/dist/runtime/result-DIjKM-p4.d.ts +0 -1
  60. package/dist/runtime/schema-B9XxyRO8.js.map +0 -1
package/dist/cli/bin.mjs CHANGED
@@ -3,10 +3,10 @@ import { createRequire, isBuiltin } from "node:module";
3
3
  import * as NFS from "node:fs";
4
4
  import { writeSync } from "node:fs";
5
5
  import process$1 from "node:process";
6
- import * as Deferred from "effect/Deferred";
6
+ import * as Deferred$1 from "effect/Deferred";
7
7
  import * as Effect$1 from "effect/Effect";
8
8
  import { effectify } from "effect/Effect";
9
- import * as Exit from "effect/Exit";
9
+ import * as Exit$1 from "effect/Exit";
10
10
  import * as FileSystem$1 from "effect/FileSystem";
11
11
  import * as Layer$1 from "effect/Layer";
12
12
  import * as Path$3 from "effect/Path";
@@ -71,7 +71,7 @@ import * as RcRef from "effect/RcRef";
71
71
  import * as Terminal$1 from "effect/Terminal";
72
72
  import * as readline from "node:readline";
73
73
  import * as Runtime from "effect/Runtime";
74
- import { Array as Array$1, Brand, Cause, Config, ConfigProvider, Context, Crypto, DateTime, Effect, Effectable, Encoding, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Redacted, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Struct, Terminal } from "effect";
74
+ import { Array as Array$1, Brand, Cause, Clock, Config, ConfigProvider, Context, Crypto, DateTime, Deferred, Effect, Effectable, Encoding, Exit, FiberSet, FileSystem, Function as Function$1, HashMap, HashSet, Layer, ManagedRuntime, MutableHashMap, MutableHashSet, Option, Path as Path$1, Predicate, PubSub, Record, Redacted, Ref, Result, Schedule, Schema, SchemaGetter, Semaphore, Stream, String as String$1, Struct, Terminal } from "effect";
75
75
  import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
76
76
  import { customAlphabet } from "nanoid";
77
77
  import { createAuthClient } from "better-auth/client";
@@ -371,11 +371,11 @@ const readableToPullUnsafe = (options) => {
371
371
  latch.openUnsafe();
372
372
  }
373
373
  function onError(error) {
374
- exit.current = Exit.fail(options.onError(error));
374
+ exit.current = Exit$1.fail(options.onError(error));
375
375
  latch.openUnsafe();
376
376
  }
377
377
  function onEnd() {
378
- exit.current = Exit.fail(Cause$1.Done());
378
+ exit.current = Exit$1.fail(Cause$1.Done());
379
379
  latch.openUnsafe();
380
380
  }
381
381
  readable.on("readable", onReadable);
@@ -420,7 +420,7 @@ var StreamAdapter = class extends Readable {
420
420
  }))).pipe(this.readLatch.whenOpen, Effect$1.provideContext(context), Effect$1.runFork);
421
421
  this.fiber.addObserver((exit) => {
422
422
  this.fiber = void 0;
423
- if (Exit.isSuccess(exit)) this.push(null);
423
+ if (Exit$1.isSuccess(exit)) this.push(null);
424
424
  else this.destroy(Cause$1.squash(exit.cause));
425
425
  });
426
426
  }
@@ -602,13 +602,13 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
602
602
  };
603
603
  };
604
604
  const spawn = (command, spawnOptions) => Effect$1.callback((resume) => {
605
- const deferred = Deferred.makeUnsafe();
605
+ const deferred = Deferred$1.makeUnsafe();
606
606
  const handle = NodeChildProcess.spawn(command.command, command.args, spawnOptions);
607
607
  handle.on("error", (error) => {
608
608
  resume(Effect$1.fail(toPlatformError("spawn", error, command)));
609
609
  });
610
610
  handle.on("exit", (...args) => {
611
- Deferred.doneUnsafe(deferred, Exit.succeed(args));
611
+ Deferred$1.doneUnsafe(deferred, Exit$1.succeed(args));
612
612
  });
613
613
  handle.on("spawn", () => {
614
614
  resume(Effect$1.succeed([handle, deferred]));
@@ -687,15 +687,15 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
687
687
  env,
688
688
  stdio
689
689
  }, process.platform)), Effect$1.fnUntraced(function* ([childProcess, exitSignal]) {
690
- const exited = yield* Deferred.isDone(exitSignal);
690
+ const exited = yield* Deferred$1.isDone(exitSignal);
691
691
  const killWithTimeout = withTimeout(childProcess, cmd, cmd.options);
692
692
  if (exited) {
693
- const [code] = yield* Deferred.await(exitSignal);
693
+ const [code] = yield* Deferred$1.await(exitSignal);
694
694
  if (code !== 0 && Predicate$1.isNotNull(code)) return yield* Effect$1.ignore(killWithTimeout(killProcessGroup));
695
695
  return yield* Effect$1.void;
696
696
  }
697
697
  if (!isReferenced) return yield* Effect$1.void;
698
- return yield* killWithTimeout((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred.await(exitSignal)))).pipe(Effect$1.ignore);
698
+ return yield* killWithTimeout((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred$1.await(exitSignal)))).pipe(Effect$1.ignore);
699
699
  }));
700
700
  const pid = ProcessId(childProcess.pid);
701
701
  childProcess.on("exit", (code) => {
@@ -717,14 +717,14 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
717
717
  const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig);
718
718
  const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig);
719
719
  const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds);
720
- const isRunning = Effect$1.map(Deferred.isDone(exitSignal), (done) => !done);
721
- const exitCode = Effect$1.flatMap(Deferred.await(exitSignal), ([code, signal]) => {
720
+ const isRunning = Effect$1.map(Deferred$1.isDone(exitSignal), (done) => !done);
721
+ const exitCode = Effect$1.flatMap(Deferred$1.await(exitSignal), ([code, signal]) => {
722
722
  if (Predicate$1.isNotNull(code)) return Effect$1.succeed(ExitCode(code));
723
723
  const error = new globalThis.Error(`Process interrupted due to receipt of signal: '${signal}'`);
724
724
  return Effect$1.fail(toPlatformError("exitCode", error, cmd));
725
725
  });
726
726
  const kill = (options) => {
727
- return withTimeout(childProcess, cmd, options)((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred.await(exitSignal)))).pipe(Effect$1.asVoid);
727
+ return withTimeout(childProcess, cmd, options)((command, childProcess, signal) => killProcessGroup(command, childProcess, signal).pipe(Effect$1.catch(() => killProcess(command, childProcess, signal)), Effect$1.andThen(Deferred$1.await(exitSignal)))).pipe(Effect$1.asVoid);
728
728
  };
729
729
  return makeHandle({
730
730
  pid,
@@ -4518,7 +4518,7 @@ const access = /*#__PURE__*/ (() => {
4518
4518
  return nodeAccess(path, mode);
4519
4519
  };
4520
4520
  })();
4521
- const copy = /*#__PURE__*/ (() => {
4521
+ const copy$1 = /*#__PURE__*/ (() => {
4522
4522
  const nodeCp = /*#__PURE__*/ effectify(NFS.cp, /*#__PURE__*/ handleErrnoException("FileSystem", "copy"), /*#__PURE__*/ handleBadArgument("copy"));
4523
4523
  return (fromPath, toPath, options) => nodeCp(fromPath, toPath, {
4524
4524
  force: options?.overwrite ?? false,
@@ -4820,7 +4820,7 @@ const makeFileSystem = /*#__PURE__*/ Effect$1.map(/*#__PURE__*/ Effect$1.service
4820
4820
  access,
4821
4821
  chmod,
4822
4822
  chown,
4823
- copy,
4823
+ copy: copy$1,
4824
4824
  copyFile,
4825
4825
  glob,
4826
4826
  link,
@@ -29413,11 +29413,32 @@ defineId("conn", "ignotum/gateway/ConnectionId");
29413
29413
  const RuntimeRequestNonce = defineId("nonce", "ignotum/runtime/RequestNonce");
29414
29414
  const RequestId = defineId("req", "ignotum/runtime/RequestId");
29415
29415
  const DevDatabaseLockId = defineId("lock", "ignotum/dev/DatabaseLockId");
29416
+ defineId("iid", "ignotum/id/AppUserId");
29417
+ defineId("idu", "ignotum/id/GlobalUserId");
29418
+ defineId("ids", "ignotum/id/SessionId");
29419
+ defineId("idr", "ignotum/id/RequestId");
29420
+ defineId("epoch", "ignotum/id/SessionEpoch");
29416
29421
  //#endregion
29417
29422
  //#region ../contracts/dist/runtime/identity.js
29418
29423
  const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
29419
29424
  const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
29420
29425
  const AppStateRevision = Schema.Natural.pipe(Schema.brand("ignotum/hosted/AppStateRevision"));
29426
+ //#endregion
29427
+ //#region ../contracts/dist/versioned.js
29428
+ /** Registers the first revision of a long-lived format. */
29429
+ const initial = (schema) => schema;
29430
+ /** Adds one adjacent migration and keeps encoding on the new current revision. */
29431
+ const upgrade = (previous, current, migrate) => {
29432
+ const accepted = Schema.Union([previous, current]);
29433
+ const isCurrent = Schema.is(current);
29434
+ return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
29435
+ decode: SchemaGetter.transform((value) => {
29436
+ if (isCurrent(value)) return value;
29437
+ return migrate(value);
29438
+ }),
29439
+ encode: SchemaGetter.transform((value) => value)
29440
+ }));
29441
+ };
29421
29442
  const FileFormat = Schema.Literals([
29422
29443
  "jpeg",
29423
29444
  "png",
@@ -29571,6 +29592,158 @@ const datePathsOf = (value) => {
29571
29592
  };
29572
29593
  const datePathsOfObject = (value) => datePathsOf(Schema.decodeUnknownSync(TransportObjectTypeSchema)(value));
29573
29594
  //#endregion
29595
+ //#region ../contracts/dist/json.js
29596
+ const encodeJson = (value) => {
29597
+ if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
29598
+ if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
29599
+ return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String$1.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
29600
+ };
29601
+ const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
29602
+ //#endregion
29603
+ //#region ../contracts/dist/runtime/result.js
29604
+ const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
29605
+ const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
29606
+ const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
29607
+ const ErrorBrand = "ignotum/error";
29608
+ Brand.nominal();
29609
+ const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
29610
+ var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: RequestId }) {};
29611
+ const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
29612
+ const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
29613
+ function makeResultEffectBase() {
29614
+ const Base = function() {};
29615
+ Base.prototype = Effectable.Prototype({
29616
+ label: "IgnotumResult",
29617
+ evaluate() {
29618
+ return this[ResultTypeId];
29619
+ }
29620
+ });
29621
+ return Base;
29622
+ }
29623
+ const ResultEffectBase = makeResultEffectBase();
29624
+ var ResultOperation = class ResultOperation extends ResultEffectBase {
29625
+ [ResultTypeId];
29626
+ constructor(effect, completed) {
29627
+ super();
29628
+ this[ResultTypeId] = effect;
29629
+ if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
29630
+ }
29631
+ catch(cases) {
29632
+ const effectCases = {};
29633
+ for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
29634
+ const runtimeEffect = this[ResultTypeId];
29635
+ const caught = Effect.catchTags(runtimeEffect, effectCases);
29636
+ return new ResultOperation(caught);
29637
+ }
29638
+ };
29639
+ var PendingQueryResult = class {
29640
+ [PendingResultTypeId] = PendingResultTypeId;
29641
+ };
29642
+ const inspectResult = (value) => {
29643
+ if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
29644
+ return value[CompletedResultTypeId];
29645
+ };
29646
+ const resultFromEffect = (effect) => new ResultOperation(effect);
29647
+ const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
29648
+ Function$1.dual(2, (result, matchers) => {
29649
+ if (result instanceof PendingQueryResult) {
29650
+ if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
29651
+ return matchers.pending();
29652
+ }
29653
+ if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
29654
+ const inspected = inspectResult(result);
29655
+ if (inspected?.type === "Success") {
29656
+ const value = inspected.value;
29657
+ return matchers.value(value);
29658
+ }
29659
+ if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
29660
+ const error = result[CompletedResultTypeId].error;
29661
+ if (error._tag === "InternalServerError") {
29662
+ const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
29663
+ const handler = matchers.internalError;
29664
+ if (handler === void 0) throw internalError;
29665
+ return handler(internalError);
29666
+ }
29667
+ const errorMatcher = matchers.error;
29668
+ if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
29669
+ if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
29670
+ const handlers = errorMatcher;
29671
+ const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
29672
+ if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
29673
+ return handler(error);
29674
+ });
29675
+ const documentNotFound = (table, id) => Brand.nominal()({
29676
+ _tag: "DocumentNotFound",
29677
+ table,
29678
+ id
29679
+ });
29680
+ //#endregion
29681
+ //#region ../contracts/dist/id.js
29682
+ const DevelopmentUsername = Schema.String.check(Schema.isPattern(/^[a-z][a-z0-9_-]{0,63}$/));
29683
+ const UserId = DevelopmentUsername.pipe(Schema.brand("ignotum/id/UserId"));
29684
+ const User = Schema.Struct({
29685
+ id: UserId,
29686
+ name: Schema.optional(Schema.String),
29687
+ email: Schema.optional(Schema.String),
29688
+ image: Schema.optional(Schema.String)
29689
+ });
29690
+ const ProfileField = Schema.Literals([
29691
+ "name",
29692
+ "email",
29693
+ "image"
29694
+ ]);
29695
+ const ProfileFields = Schema.Array(ProfileField).check(Schema.isUnique());
29696
+ const IdRequired = Schema.TaggedStruct("IdRequired", {}).pipe(Schema.brand(ErrorBrand));
29697
+ const makeIdContext = (read) => ({
29698
+ current: () => resultFromEffect(read),
29699
+ require: () => resultFromEffect(read.pipe(Effect.flatMap((user) => user === null ? Effect.fail(IdRequired.make({})) : Effect.succeed(user))))
29700
+ });
29701
+ const SessionState = Schema.Struct({
29702
+ sessionEpoch: Schema.String,
29703
+ viewRevision: Schema.Natural,
29704
+ user: Schema.NullOr(User),
29705
+ validUntil: Schema.Finite
29706
+ });
29707
+ Schema.Struct({
29708
+ development: Schema.optional(Schema.Literal(true)),
29709
+ outcome: Schema.optional(Schema.Literals([
29710
+ "SignedIn",
29711
+ "ProfileUpdated",
29712
+ "Cancelled"
29713
+ ])),
29714
+ ...SessionState.fields,
29715
+ serverTime: Schema.Finite,
29716
+ expiresAt: Schema.Finite
29717
+ });
29718
+ const ReturnPath = Schema.String.check(Schema.makeFilter((value) => {
29719
+ if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return false;
29720
+ const url = new URL(value, "https://app.ignotum.invalid");
29721
+ return url.origin === "https://app.ignotum.invalid" && url.pathname !== "/_ignotum" && !url.pathname.startsWith("/_ignotum/");
29722
+ }, { message: "Return to a path within this app." }));
29723
+ const SessionRequest = Schema.Struct({
29724
+ intent: Schema.Literals(["signIn", "requestProfile"]),
29725
+ profile: ProfileFields,
29726
+ returnTo: ReturnPath,
29727
+ sessionEpoch: Schema.String
29728
+ });
29729
+ Schema.Struct({
29730
+ redirectUrl: Schema.String,
29731
+ expiresAt: Schema.Finite
29732
+ });
29733
+ Schema.Literals([
29734
+ "SignedIn",
29735
+ "ProfileUpdated",
29736
+ "Cancelled"
29737
+ ]);
29738
+ const sessionRequestsPath = `/_ignotum/v1/session/requests`;
29739
+ const idViewKey = (session) => encodeCanonicalJson([session.user, session.viewRevision]);
29740
+ Schema.Struct({
29741
+ formatVersion: Schema.Literal(1),
29742
+ username: Schema.NullOr(DevelopmentUsername),
29743
+ epoch: Schema.String
29744
+ });
29745
+ const developmentSelectionKey = "ignotum.id.development";
29746
+ //#endregion
29574
29747
  //#region ../contracts/dist/runtime/sync.js
29575
29748
  const FunctionNamePart = Schema.String.check(Schema.isPattern(/^[A-Za-z_$][A-Za-z0-9_$]*$/));
29576
29749
  const ApiFunctionAddressParts = Schema.TemplateLiteralParser([
@@ -29646,12 +29819,19 @@ const WireFailure = Schema.Struct({
29646
29819
  });
29647
29820
  const WireResult = Schema.Union([WireSuccess, WireFailure]);
29648
29821
  const SyncHandshake = Schema.Struct({
29822
+ session: Schema.optional(SessionState),
29823
+ serverTime: Schema.optional(Schema.Finite),
29649
29824
  type: Schema.Literal("Handshake"),
29650
29825
  appId: AppId,
29651
29826
  deploymentId: DeploymentId,
29652
29827
  generation: DeploymentGeneration
29653
29828
  });
29654
29829
  const Snapshot = Schema.Struct({
29830
+ identity: Schema.optional(Schema.Struct({
29831
+ sessionEpoch: Schema.String,
29832
+ viewRevision: Schema.Natural,
29833
+ dependsOnId: Schema.Boolean
29834
+ })),
29655
29835
  type: Schema.Literal("Snapshot"),
29656
29836
  id: SubscriptionId,
29657
29837
  result: WireResult,
@@ -29662,6 +29842,7 @@ const Snapshot = Schema.Struct({
29662
29842
  })))
29663
29843
  });
29664
29844
  const Preparation = Schema.Struct({
29845
+ sessionEpoch: Schema.optional(Schema.String),
29665
29846
  type: Schema.Literal("Preparation"),
29666
29847
  id: InvocationId,
29667
29848
  kind: Schema.Literal("Mutation"),
@@ -29671,17 +29852,21 @@ const Preparation = Schema.Struct({
29671
29852
  }))
29672
29853
  });
29673
29854
  const SyncResultSuccess = Schema.Struct({
29855
+ sessionEpoch: Schema.optional(Schema.String),
29674
29856
  type: Schema.Literal("Result"),
29675
29857
  id: InvocationId,
29676
29858
  result: WireSuccess,
29677
29859
  committedRevision: AppStateRevision
29678
29860
  });
29679
29861
  const SyncResultFailure = Schema.Struct({
29862
+ sessionEpoch: Schema.optional(Schema.String),
29680
29863
  type: Schema.Literal("Result"),
29681
29864
  id: InvocationId,
29682
29865
  result: WireFailure
29683
29866
  });
29684
29867
  const ErrorCode = Schema.Literals([
29868
+ "SessionChanged",
29869
+ "SessionExpired",
29685
29870
  "DuplicateOperationId",
29686
29871
  "FunctionUnavailable",
29687
29872
  "InvalidArguments",
@@ -29692,6 +29877,7 @@ const ErrorCode = Schema.Literals([
29692
29877
  "WrongFunctionKind"
29693
29878
  ]);
29694
29879
  const SyncError = Schema.Struct({
29880
+ sessionEpoch: Schema.optional(Schema.String),
29695
29881
  type: Schema.Literal("Error"),
29696
29882
  operation: Schema.optional(Operation),
29697
29883
  code: ErrorCode,
@@ -29702,7 +29888,30 @@ const Deployment = Schema.Struct({
29702
29888
  deploymentId: DeploymentId,
29703
29889
  generation: DeploymentGeneration
29704
29890
  });
29891
+ const Session = Schema.Union([Schema.Struct({
29892
+ type: Schema.Literal("Session"),
29893
+ event: Schema.Literal("Updated"),
29894
+ serverTime: Schema.Finite,
29895
+ session: SessionState
29896
+ }), Schema.Struct({
29897
+ type: Schema.Literal("Session"),
29898
+ event: Schema.Literals([
29899
+ "SignedOut",
29900
+ "Revoked",
29901
+ "Expired",
29902
+ "Unavailable",
29903
+ "Replaced"
29904
+ ]),
29905
+ sessionEpoch: Schema.String
29906
+ })]);
29907
+ const Invalidate = Schema.Struct({
29908
+ type: Schema.Literal("Invalidate"),
29909
+ sessionEpoch: Schema.String,
29910
+ ids: Schema.Array(SubscriptionId)
29911
+ });
29705
29912
  const ServerMessage = Schema.Union([
29913
+ Session,
29914
+ Invalidate,
29706
29915
  SyncHandshake,
29707
29916
  Snapshot,
29708
29917
  Preparation,
@@ -29736,7 +29945,151 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
29736
29945
  message: Schema.String
29737
29946
  }) {};
29738
29947
  //#endregion
29739
- //#region ../contracts/dist/descriptor-XzDX2JDw.js
29948
+ //#region ../contracts/dist/descriptor-DCCz5AHd.js
29949
+ const ValueEvolutionTypeId = Symbol.for("ignotum/schema/ValueEvolution");
29950
+ const evolutionRules = (value) => value[ValueEvolutionTypeId] ?? {};
29951
+ const codec = (value) => Schema.toCodecJson(value);
29952
+ const encode = (schema, value) => Schema.decodeUnknownSync(Schema.Json)(Schema.encodeUnknownSync(codec(schema), { onExcessProperty: "error" })(value));
29953
+ const decode = (schema, value) => Schema.decodeUnknownSync(codec(schema), { onExcessProperty: "error" })(value);
29954
+ const copy = (value) => Object.create(Object.getPrototypeOf(value), Object.getOwnPropertyDescriptors(value));
29955
+ const isValueObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown));
29956
+ const attachEvolution = (value, rules = {}) => {
29957
+ Object.defineProperty(value, ValueEvolutionTypeId, {
29958
+ configurable: true,
29959
+ value: rules
29960
+ });
29961
+ return Object.assign(value, {
29962
+ default(defaultValue) {
29963
+ if (getValueDescriptor(value)?.type === "optional") throw new Error("Defaults require a non-optional validator. A default already allows omission on creation.");
29964
+ const defaultJson = JSON.stringify(encode(value, defaultValue));
29965
+ const next = copy(value);
29966
+ const constructor = Schema.withConstructorDefault(Effect.sync(() => decode(value, JSON.parse(defaultJson))))(value);
29967
+ Object.defineProperty(next, "ast", {
29968
+ value: constructor.ast,
29969
+ configurable: true
29970
+ });
29971
+ return attachEvolution(next, {
29972
+ ...rules,
29973
+ defaultJson
29974
+ });
29975
+ },
29976
+ renameFrom(name) {
29977
+ if (name.length === 0 || [
29978
+ "id",
29979
+ "createdAt",
29980
+ "updatedAt"
29981
+ ].includes(name)) throw new Error(`Cannot rename reserved or empty field '${name}'.`);
29982
+ return attachEvolution(copy(value), {
29983
+ ...rules,
29984
+ renameFrom: name
29985
+ });
29986
+ },
29987
+ from(previous, convert) {
29988
+ if (getValueDescriptor(previous) === void 0) throw new Error("Historical values must use an Ignotum values validator.");
29989
+ return attachEvolution(copy(value), {
29990
+ ...rules,
29991
+ previous: [...rules.previous ?? [], {
29992
+ schema: previous,
29993
+ convert
29994
+ }]
29995
+ });
29996
+ }
29997
+ });
29998
+ };
29999
+ const withValueChildren = (value, children) => {
30000
+ attachEvolution(value, {
30001
+ ...evolutionRules(value),
30002
+ children
30003
+ });
30004
+ return value;
30005
+ };
30006
+ const validateFieldRenames = (fields) => {
30007
+ const sources = /* @__PURE__ */ new Set();
30008
+ for (const [name, field] of Object.entries(fields)) {
30009
+ const source = evolutionRules(field).renameFrom;
30010
+ if (source === void 0) continue;
30011
+ if (source === name || Object.hasOwn(fields, source) || sources.has(source)) throw new Error(`Rename source '${source}' conflicts with another field.`);
30012
+ sources.add(source);
30013
+ }
30014
+ };
30015
+ const mapChildren = (schema, value, visit, rename) => {
30016
+ const children = evolutionRules(schema).children;
30017
+ if (children === void 0) return value;
30018
+ switch (children.type) {
30019
+ case "object": {
30020
+ if (!isValueObject(value)) return value;
30021
+ const result = { ...value };
30022
+ for (const [name, field] of Object.entries(children.fields)) {
30023
+ const source = rename ? evolutionRules(field).renameFrom : void 0;
30024
+ if (source !== void 0 && Object.hasOwn(result, source)) {
30025
+ if (Object.hasOwn(result, name)) throw new Error(`Both '${name}' and its rename source '${source}' are present.`);
30026
+ Object.defineProperty(result, name, {
30027
+ value: result[source],
30028
+ enumerable: true,
30029
+ configurable: true,
30030
+ writable: true
30031
+ });
30032
+ delete result[source];
30033
+ }
30034
+ const next = visit(field, Object.hasOwn(result, name) ? result[name] : void 0);
30035
+ if (next !== void 0) Object.defineProperty(result, name, {
30036
+ value: next,
30037
+ enumerable: true,
30038
+ configurable: true,
30039
+ writable: true
30040
+ });
30041
+ }
30042
+ return result;
30043
+ }
30044
+ case "array": return Array.isArray(value) ? value.map((item) => visit(children.value, item)) : value;
30045
+ case "record": return isValueObject(value) ? Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(children.value, item)])) : value;
30046
+ case "nullable": return value === null ? value : visit(children.value, value);
30047
+ case "optional": return value === void 0 ? value : visit(children.value, value);
30048
+ case "union":
30049
+ for (const member of children.members) try {
30050
+ const result = visit(member, value);
30051
+ if (rename) decode(member, result);
30052
+ else encode(member, result);
30053
+ return result;
30054
+ } catch {}
30055
+ return value;
30056
+ }
30057
+ };
30058
+ /** Apply creation defaults to decoded values, without accepting historical representations. */
30059
+ const applyValueDefaults = (schema, value) => {
30060
+ const rules = evolutionRules(schema);
30061
+ if (value === void 0 && rules.defaultJson !== void 0) return decode(schema, JSON.parse(rules.defaultJson));
30062
+ return mapChildren(schema, value, applyValueDefaults, false);
30063
+ };
30064
+ /** Convert a stored JSON value to the current representation, preserving already valid values. */
30065
+ const evolveStoredValue = (schema, value) => {
30066
+ try {
30067
+ decode(schema, value);
30068
+ return value;
30069
+ } catch {}
30070
+ const rules = evolutionRules(schema);
30071
+ if (value === void 0 && rules.defaultJson !== void 0) return JSON.parse(rules.defaultJson);
30072
+ let failure;
30073
+ try {
30074
+ const next = mapChildren(schema, value, evolveStoredValue, true);
30075
+ decode(schema, next);
30076
+ return next;
30077
+ } catch (cause) {
30078
+ failure = cause;
30079
+ }
30080
+ const matches = [];
30081
+ for (const previous of rules.previous ?? []) try {
30082
+ matches.push({
30083
+ previous,
30084
+ value: decode(previous.schema, value)
30085
+ });
30086
+ } catch {}
30087
+ if (matches.length > 1) throw new Error("More than one historical validator matches this value.");
30088
+ const match = matches[0];
30089
+ if (match === void 0) throw failure;
30090
+ return encode(schema, applyValueDefaults(schema, match.previous.convert(match.value)));
30091
+ };
30092
+ const evolveStoredFields = (schema, value) => Schema.decodeUnknownSync(Schema.JsonObject)(evolveStoredValue(schema, value));
29740
30093
  const LiteralValue = Schema.Union([
29741
30094
  Schema.String,
29742
30095
  Schema.Finite,
@@ -29773,6 +30126,7 @@ const ValueDescriptor = Schema.Union([
29773
30126
  type: Schema.Literal("id"),
29774
30127
  table: Schema.String
29775
30128
  }),
30129
+ Schema.Struct({ type: Schema.Literal("userId") }),
29776
30130
  Schema.Struct({ type: Schema.Literal("integer") }),
29777
30131
  Schema.Struct({
29778
30132
  type: Schema.Literal("literal"),
@@ -29819,7 +30173,7 @@ const attachValueDescriptor = Function$1.dual(2, (value, descriptor) => {
29819
30173
  value: descriptor,
29820
30174
  writable: false
29821
30175
  });
29822
- return value;
30176
+ return attachEvolution(value);
29823
30177
  });
29824
30178
  const getValueDescriptor = (value) => {
29825
30179
  if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
@@ -29843,6 +30197,7 @@ const descriptorAllowedInEnvironment = (descriptor) => {
29843
30197
  switch (descriptor.type) {
29844
30198
  case "error":
29845
30199
  case "file":
30200
+ case "userId":
29846
30201
  case "id": return false;
29847
30202
  case "array":
29848
30203
  case "nullable":
@@ -29869,22 +30224,6 @@ const descriptorFields = (fields) => {
29869
30224
  return entries.sort((left, right) => left.name.localeCompare(right.name));
29870
30225
  };
29871
30226
  //#endregion
29872
- //#region ../contracts/dist/versioned.js
29873
- /** Registers the first revision of a long-lived format. */
29874
- const initial = (schema) => schema;
29875
- /** Adds one adjacent migration and keeps encoding on the new current revision. */
29876
- const upgrade = (previous, current, migrate) => {
29877
- const accepted = Schema.Union([previous, current]);
29878
- const isCurrent = Schema.is(current);
29879
- return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
29880
- decode: SchemaGetter.transform((value) => {
29881
- if (isCurrent(value)) return value;
29882
- return migrate(value);
29883
- }),
29884
- encode: SchemaGetter.transform((value) => value)
29885
- }));
29886
- };
29887
- //#endregion
29888
30227
  //#region ../contracts/dist/deployment.js
29889
30228
  const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
29890
30229
  const ClientPath = Schema.String.check(Schema.isPattern(/^\/(?!_ignotum(?:\/|$))(?:(?:[A-Za-z0-9._~-]+\/)*[A-Za-z0-9._~-]+\/?)?$/)).pipe(Schema.brand("ignotum/deployment/ClientPath"));
@@ -30010,7 +30349,7 @@ const ServerBuildManifestV2 = Schema.Struct({
30010
30349
  schema: ArtifactReference,
30011
30350
  functions: Schema.Array(ServerFunctionArtifact)
30012
30351
  });
30013
- const ServerBuildManifest = upgrade(upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
30352
+ const ServerBuildManifest = upgrade(upgrade(upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
30014
30353
  formatVersion: 2,
30015
30354
  schema: manifest.schema,
30016
30355
  functions: manifest.functions
@@ -30023,6 +30362,18 @@ const ServerBuildManifest = upgrade(upgrade(initial(ServerBuildManifestV1), Serv
30023
30362
  formatVersion: 3,
30024
30363
  schema: manifest.schema,
30025
30364
  functions: manifest.functions
30365
+ })), Schema.Struct({
30366
+ formatVersion: Schema.Literal(4),
30367
+ schema: ArtifactReference,
30368
+ environment: Schema.optional(ServerEnvironmentArtifact),
30369
+ functions: Schema.Array(ServerFunctionArtifact),
30370
+ schemaEvolution: Schema.optional(Schema.Struct({
30371
+ bundle: ArtifactReference,
30372
+ sourceMap: ArtifactReference
30373
+ }))
30374
+ }), (manifest) => ({
30375
+ ...manifest,
30376
+ formatVersion: 4
30026
30377
  }));
30027
30378
  const deploymentInventoryPath = ArtifactPath.make("inventory.json");
30028
30379
  const clientManifestPath = ArtifactPath.make("client/manifest.json");
@@ -30078,7 +30429,8 @@ Schema.Struct({
30078
30429
  previousDeploymentId: Schema.NullOr(DeploymentId),
30079
30430
  previousGeneration: DeploymentGeneration,
30080
30431
  deploymentId: DeploymentId,
30081
- generation: DeploymentGeneration
30432
+ generation: DeploymentGeneration,
30433
+ schemaPreparation: Schema.optional(Schema.Boolean)
30082
30434
  });
30083
30435
  const CreateAppRequest = Schema.Struct({ slug: AppSlug });
30084
30436
  const BeginDeploymentRequest = Schema.Struct({ inventory: DeploymentInventory });
@@ -30140,7 +30492,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
30140
30492
  };
30141
30493
  //#endregion
30142
30494
  //#region package.json
30143
- var version = "0.0.12";
30495
+ var version = "0.0.14";
30144
30496
  //#endregion
30145
30497
  //#region src/cli/codegen.ts
30146
30498
  const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
@@ -30578,92 +30930,6 @@ const controlClientLayer = Layer.effect(ControlClient, Effect.gen(function* () {
30578
30930
  });
30579
30931
  }));
30580
30932
  //#endregion
30581
- //#region ../contracts/dist/json.js
30582
- const encodeJson = (value) => {
30583
- if (Array$1.isArray(value)) return `[${value.map(encodeJson).join(",")}]`;
30584
- if (value === null || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return JSON.stringify(value);
30585
- return `{${Array$1.sortWith(Object.entries(value), ([key]) => key, String$1.Order).map(([key, field]) => `${JSON.stringify(key)}:${encodeJson(field)}`).join(",")}}`;
30586
- };
30587
- const encodeCanonicalJson = (value) => encodeJson(Schema.decodeSync(Schema.Json)(value));
30588
- //#endregion
30589
- //#region ../contracts/dist/runtime/result.js
30590
- const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
30591
- const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
30592
- const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
30593
- const ErrorBrand = "ignotum/error";
30594
- Brand.nominal();
30595
- const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
30596
- var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: RequestId }) {};
30597
- const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
30598
- const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
30599
- function makeResultEffectBase() {
30600
- const Base = function() {};
30601
- Base.prototype = Effectable.Prototype({
30602
- label: "IgnotumResult",
30603
- evaluate() {
30604
- return this[ResultTypeId];
30605
- }
30606
- });
30607
- return Base;
30608
- }
30609
- const ResultEffectBase = makeResultEffectBase();
30610
- var ResultOperation = class ResultOperation extends ResultEffectBase {
30611
- [ResultTypeId];
30612
- constructor(effect, completed) {
30613
- super();
30614
- this[ResultTypeId] = effect;
30615
- if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
30616
- }
30617
- catch(cases) {
30618
- const effectCases = {};
30619
- for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
30620
- const runtimeEffect = this[ResultTypeId];
30621
- const caught = Effect.catchTags(runtimeEffect, effectCases);
30622
- return new ResultOperation(caught);
30623
- }
30624
- };
30625
- var PendingQueryResult = class {
30626
- [PendingResultTypeId] = PendingResultTypeId;
30627
- };
30628
- const inspectResult = (value) => {
30629
- if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
30630
- return value[CompletedResultTypeId];
30631
- };
30632
- const resultFromEffect = (effect) => new ResultOperation(effect);
30633
- const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
30634
- Function$1.dual(2, (result, matchers) => {
30635
- if (result instanceof PendingQueryResult) {
30636
- if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
30637
- return matchers.pending();
30638
- }
30639
- if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
30640
- const inspected = inspectResult(result);
30641
- if (inspected?.type === "Success") {
30642
- const value = inspected.value;
30643
- return matchers.value(value);
30644
- }
30645
- if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
30646
- const error = result[CompletedResultTypeId].error;
30647
- if (error._tag === "InternalServerError") {
30648
- const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
30649
- const handler = matchers.internalError;
30650
- if (handler === void 0) throw internalError;
30651
- return handler(internalError);
30652
- }
30653
- const errorMatcher = matchers.error;
30654
- if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
30655
- if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
30656
- const handlers = errorMatcher;
30657
- const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
30658
- if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
30659
- return handler(error);
30660
- });
30661
- const documentNotFound = (table, id) => Brand.nominal()({
30662
- _tag: "DocumentNotFound",
30663
- table,
30664
- id
30665
- });
30666
- //#endregion
30667
30933
  //#region ../contracts/dist/runtime/pagination.js
30668
30934
  const PaginationCursor = Schema.String.check(Schema.isBase64Url()).pipe(Schema.brand("ignotum/runtime/PaginationCursor"));
30669
30935
  const PaginationPageSize = Schema.Int.check(Schema.isBetween({
@@ -30745,15 +31011,19 @@ const requireDescriptor = (value) => {
30745
31011
  return descriptor;
30746
31012
  };
30747
31013
  const array = (value) => {
30748
- return attachValueDescriptor(Schema.Array(value), {
31014
+ return withValueChildren(attachValueDescriptor(Schema.Array(value), {
30749
31015
  type: "array",
30750
31016
  value: requireDescriptor(value)
31017
+ }), {
31018
+ type: "array",
31019
+ value
30751
31020
  });
30752
31021
  };
30753
31022
  const assertFieldsExist = (fields, keys, operation) => {
30754
31023
  for (const key of keys) if (!Object.hasOwn(fields, key)) throw new Error(`Cannot ${operation} unknown field '${String(key)}'.`);
30755
31024
  };
30756
31025
  const fixedObject = (fields) => {
31026
+ validateFieldRenames(fields);
30757
31027
  const pick = (...keys) => {
30758
31028
  assertFieldsExist(fields, keys, "pick");
30759
31029
  return fixedObject(Struct.pick(fields, keys));
@@ -30783,9 +31053,12 @@ const fixedObject = (fields) => {
30783
31053
  }
30784
31054
  return fixedObject(partialFields);
30785
31055
  };
30786
- const schema = attachValueDescriptor(Schema.Struct(fields), {
31056
+ const schema = withValueChildren(attachValueDescriptor(Schema.Struct(fields), {
30787
31057
  type: "object",
30788
31058
  fields: descriptorFields(fields)
31059
+ }), {
31060
+ type: "object",
31061
+ fields
30789
31062
  });
30790
31063
  return Object.assign(schema, {
30791
31064
  extend,
@@ -30797,27 +31070,39 @@ const fixedObject = (fields) => {
30797
31070
  };
30798
31071
  const object = (fields) => fixedObject(fields);
30799
31072
  const optional = (value) => {
30800
- return attachValueDescriptor(Schema.optional(value), {
31073
+ return withValueChildren(attachValueDescriptor(Schema.optional(value), {
30801
31074
  type: "optional",
30802
31075
  value: requireDescriptor(value)
31076
+ }), {
31077
+ type: "optional",
31078
+ value
30803
31079
  });
30804
31080
  };
30805
31081
  const nullable = (value) => {
30806
- return attachValueDescriptor(Schema.NullOr(value), {
31082
+ return withValueChildren(attachValueDescriptor(Schema.NullOr(value), {
30807
31083
  type: "nullable",
30808
31084
  value: requireDescriptor(value)
31085
+ }), {
31086
+ type: "nullable",
31087
+ value
30809
31088
  });
30810
31089
  };
30811
31090
  const record = (value) => {
30812
- return attachValueDescriptor(Schema.Record(Schema.String, value), {
31091
+ return withValueChildren(attachValueDescriptor(Schema.Record(Schema.String, value), {
30813
31092
  type: "record",
30814
31093
  value: requireDescriptor(value)
31094
+ }), {
31095
+ type: "record",
31096
+ value
30815
31097
  });
30816
31098
  };
30817
31099
  const union = (...members) => {
30818
- return attachValueDescriptor(Schema.Union(members), {
31100
+ return withValueChildren(attachValueDescriptor(Schema.Union(members), {
30819
31101
  type: "union",
30820
31102
  members: members.map(requireDescriptor)
31103
+ }), {
31104
+ type: "union",
31105
+ members
30821
31106
  });
30822
31107
  };
30823
31108
  Redacted.isRedacted, Redacted.value;
@@ -31100,6 +31385,13 @@ const validateManifest = Effect.fn("Deployment.validateManifest")(function* (man
31100
31385
  references.push(manifest.environment.artifact);
31101
31386
  referencedPaths.add(serverEnvironmentPath);
31102
31387
  }
31388
+ if (manifest.schemaEvolution !== void 0) {
31389
+ const { bundle, sourceMap } = manifest.schemaEvolution;
31390
+ if (!bundle.path.startsWith("server/functions/") || !bundle.path.endsWith(".mjs") || sourceMap.path !== `${bundle.path}.map`) return yield* invalid$3("The schema conversion bundle or its source map path is invalid.");
31391
+ references.push(bundle, sourceMap);
31392
+ referencedPaths.add(bundle.path);
31393
+ referencedPaths.add(sourceMap.path);
31394
+ }
31103
31395
  for (const definition of manifest.functions) {
31104
31396
  if (addresses.has(definition.address)) return yield* invalid$3(`Duplicate function address '${definition.address}'.`);
31105
31397
  addresses.add(definition.address);
@@ -31259,18 +31551,40 @@ const makeSchemaSnapshot = (schema) => {
31259
31551
  };
31260
31552
  };
31261
31553
  const encodeSchemaSnapshot = (snapshot) => `${encodeCanonical(SchemaSnapshot, snapshot)}\n`;
31262
- Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(SchemaSnapshot, {
31263
- ...previous,
31264
- tables: previous.tables.map((table) => ({
31265
- ...table,
31266
- indexes: []
31267
- }))
31268
- }) === encodeCanonical(SchemaSnapshot, {
31269
- ...next,
31270
- tables: next.tables.map((table) => ({
31271
- ...table,
31272
- indexes: []
31273
- }))
31554
+ const acceptsPreviousValue = (previous, next) => {
31555
+ if (encodeCanonical(ValueDescriptor, previous) === encodeCanonical(ValueDescriptor, next)) return true;
31556
+ if (previous.type === "never") return true;
31557
+ if (next.type === "optional") return acceptsPreviousValue(previous.type === "optional" ? previous.value : previous, next.value);
31558
+ if (previous.type === "optional") return false;
31559
+ if (next.type === "nullable") return previous.type === "null" || acceptsPreviousValue(previous.type === "nullable" ? previous.value : previous, next.value);
31560
+ if (previous.type === "nullable") return acceptsPreviousValue({ type: "null" }, next) && acceptsPreviousValue(previous.value, next);
31561
+ if (previous.type === "union") return previous.members.every((member) => acceptsPreviousValue(member, next));
31562
+ if (next.type === "union") return next.members.some((member) => acceptsPreviousValue(previous, member));
31563
+ if (previous.type === "integer" && next.type === "number") return true;
31564
+ if (previous.type === "userId" && next.type === "string") return true;
31565
+ if (previous.type === "literals") return previous.values.every((value) => acceptsPreviousValue({
31566
+ type: "literal",
31567
+ value
31568
+ }, next));
31569
+ if (previous.type === "literal") {
31570
+ if (next.type === "literals") return next.values.includes(previous.value);
31571
+ if (next.type === "string") return Predicate.isString(previous.value);
31572
+ if (next.type === "boolean") return Predicate.isBoolean(previous.value);
31573
+ if (next.type === "number") return Predicate.isNumber(previous.value);
31574
+ if (next.type === "integer") return Predicate.isNumber(previous.value) && Number.isInteger(previous.value);
31575
+ }
31576
+ if (previous.type === "file" && next.type === "file") return previous.maxBytes <= next.maxBytes && previous.formats.every((format) => next.formats.includes(format));
31577
+ if (previous.type === "array" && next.type === "array" || previous.type === "record" && next.type === "record") return acceptsPreviousValue(previous.value, next.value);
31578
+ if (previous.type === "object" && next.type === "object") return acceptsPreviousFields(previous.fields, next.fields);
31579
+ return false;
31580
+ };
31581
+ const acceptsPreviousFields = (previous, next) => previous.every((field) => {
31582
+ const target = next.find((candidate) => candidate.name === field.name);
31583
+ return target !== void 0 && acceptsPreviousValue(field.value, target.value);
31584
+ }) && next.every((field) => previous.some((candidate) => candidate.name === field.name) || field.value.type === "optional");
31585
+ Function$1.dual(2, (previous, next) => previous === void 0 || previous.tables.every((table) => {
31586
+ const target = next.tables.find((candidate) => candidate.name === table.name);
31587
+ return target !== void 0 && acceptsPreviousFields(table.fields, target.fields);
31274
31588
  }));
31275
31589
  const AppConfiguration = initial(Schema.Struct({
31276
31590
  formatVersion: Schema.Literal(1),
@@ -31509,19 +31823,19 @@ const resolvedClientEntryId = `\0${clientEntryId}`;
31509
31823
  const clientStylesId = "virtual:ignotum/_styles.css";
31510
31824
  const resolvedClientStylesId = `\0${clientStylesId}`;
31511
31825
  const clientStylesSource = "@import \"tailwindcss\";\n";
31512
- const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
31826
+ const escapeHtml$1 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
31513
31827
  const renderClientDocument = (options) => `<!doctype html>
31514
31828
  <html lang="en">
31515
31829
  <head>
31516
31830
  <meta charset="UTF-8" />
31517
31831
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
31518
- <title>${escapeHtml(options.title)}</title>
31519
- ${options.icon === void 0 ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml(options.icon)}" />`}
31520
- ${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml(href)}" />`).join("\n")}
31832
+ <title>${escapeHtml$1(options.title)}</title>
31833
+ ${options.icon === void 0 ? "" : ` <link rel="icon" type="image/svg+xml" href="${escapeHtml$1(options.icon)}" />`}
31834
+ ${options.styles.map((href) => ` <link rel="stylesheet" crossorigin href="/${escapeHtml$1(href)}" />`).join("\n")}
31521
31835
  </head>
31522
31836
  <body>
31523
31837
  <div id="app"></div>
31524
- ${options.scripts.map((script) => script.type === "External" ? ` <script type="module" crossorigin src="/${escapeHtml(script.source)}"><\/script>` : ` <script type="module">\n ${script.source}\n <\/script>`).join("\n")}
31838
+ ${options.scripts.map((script) => script.type === "External" ? ` <script type="module" crossorigin src="/${escapeHtml$1(script.source)}"><\/script>` : ` <script type="module">\n ${script.source}\n <\/script>`).join("\n")}
31525
31839
  </body>
31526
31840
  </html>`;
31527
31841
  const renderClientEntry = (development) => `${development ? "import \"preact/debug\"\n" : ""}import { render } from "preact"
@@ -32304,16 +32618,22 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
32304
32618
  })));
32305
32619
  }));
32306
32620
  });
32307
- const serverFunctionEntryPlugin = (definition) => {
32621
+ const serverFunctionEntryPlugin = (definition, schemaPath) => {
32308
32622
  const entryId = `virtual:ignotum/server-function/${definition.address}`;
32309
32623
  const resolvedEntryId = `\0${entryId}`;
32310
- const source = `import { call } from ${encodeJavaScriptString(hostModuleId)}
32624
+ const source = schemaPath === void 0 ? `import { call } from ${encodeJavaScriptString(hostModuleId)}
32311
32625
  import { invoke } from "ignotum/internal/host"
32312
32626
  import { ${definition.exportName} as definition } from ${encodeJavaScriptString(normalizePath(definition.modulePath))}
32313
32627
 
32314
32628
  export default async function handler(encodedRequest, encodedEnvironment) {
32315
32629
  return invoke(definition, JSON.parse(encodedRequest), call, JSON.parse(encodedEnvironment))
32316
32630
  }
32631
+ ` : `import { evolve } from "ignotum/internal/host"
32632
+ import schema from ${encodeJavaScriptString(normalizePath(schemaPath))}
32633
+
32634
+ export default function handler(encodedRequest) {
32635
+ return evolve(schema, encodedRequest)
32636
+ }
32317
32637
  `;
32318
32638
  return {
32319
32639
  name: "ignotum:server-function-entry",
@@ -32321,7 +32641,7 @@ export default async function handler(encodedRequest, encodedEnvironment) {
32321
32641
  resolveId: (id) => id === entryId ? resolvedEntryId : void 0
32322
32642
  };
32323
32643
  };
32324
- const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (appDirectory, serverOutputDirectory, conditions, definition) {
32644
+ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (appDirectory, serverOutputDirectory, conditions, definition, schemaPath) {
32325
32645
  const fileSystem = yield* FileSystem.FileSystem;
32326
32646
  const path = yield* Path$1.Path;
32327
32647
  const functionDirectory = path.join(serverOutputDirectory, "functions", definition.moduleName);
@@ -32354,7 +32674,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
32354
32674
  configFile: false,
32355
32675
  logLevel: "warn",
32356
32676
  mode: "production",
32357
- plugins: [boundaries, serverFunctionEntryPlugin(definition)],
32677
+ plugins: [boundaries, serverFunctionEntryPlugin(definition, schemaPath)],
32358
32678
  publicDir: false,
32359
32679
  resolve: {
32360
32680
  conditions: [...conditions],
@@ -32367,7 +32687,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
32367
32687
  if (entries.length !== 1) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit exactly one entry chunk.` });
32368
32688
  const entry = entries[0];
32369
32689
  if (entry === void 0) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit an entry chunk.` });
32370
- if (entry.dynamicImports.length > 0 || entry.imports.length !== 1 || entry.imports[0] !== hostModuleId || !entry.exports.includes("default")) return yield* InvalidBuildOutput.make({ message: `${definition.address} is not a self-contained default-exported handler. Imports: ${entry.imports.join(", ") || "none"}; dynamic imports: ${entry.dynamicImports.join(", ") || "none"}; exports: ${entry.exports.join(", ") || "none"}.` });
32690
+ if (entry.dynamicImports.length > 0 || (schemaPath === void 0 ? entry.imports.length !== 1 || entry.imports[0] !== hostModuleId : entry.imports.length !== 0) || !entry.exports.includes("default")) return yield* InvalidBuildOutput.make({ message: `${definition.address} is not a self-contained default-exported handler. Imports: ${entry.imports.join(", ") || "none"}; dynamic imports: ${entry.dynamicImports.join(", ") || "none"}; exports: ${entry.exports.join(", ") || "none"}.` });
32371
32691
  const relativeFile = normalizePath(path.join("functions", definition.moduleName, entry.fileName));
32372
32692
  const relativeSourceMap = `${relativeFile}.map`;
32373
32693
  const absoluteFile = path.join(serverOutputDirectory, relativeFile);
@@ -32411,8 +32731,20 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
32411
32731
  const snapshotBytes = utf8Bytes(encodedSnapshot);
32412
32732
  yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
32413
32733
  const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
32734
+ const schemaPath = path.join(appDirectory, "server", "schema.ts");
32735
+ const schemaEvolution = yield* buildServerFunction(appDirectory, outputDirectory, conditions, {
32736
+ address: FunctionAddress.make("api._schema.evolve"),
32737
+ exportName: "evolve",
32738
+ kind: "Mutation",
32739
+ moduleName: "_schema",
32740
+ modulePath: schemaPath
32741
+ }, schemaPath);
32414
32742
  const manifest = {
32415
- formatVersion: 3,
32743
+ formatVersion: 4,
32744
+ schemaEvolution: {
32745
+ bundle: schemaEvolution.bundle,
32746
+ sourceMap: schemaEvolution.sourceMap
32747
+ },
32416
32748
  environment: {
32417
32749
  artifact: yield* artifactReference(serverEnvironmentPath, environmentBytes),
32418
32750
  fields: discovered.environment.fields
@@ -32450,6 +32782,11 @@ var DeploymentActivationUnconfirmed = class extends Schema.TaggedError()("Deploy
32450
32782
  deploymentId: DeploymentId,
32451
32783
  message: Schema.String
32452
32784
  }) {};
32785
+ const activationError = (deploymentId, cause) => cause._tag === "ControlClientUnavailable" || cause.status >= 500 ? DeploymentActivationUnconfirmed.make({
32786
+ cause,
32787
+ deploymentId,
32788
+ message: `Deployment ${deploymentId} is ready, but activation could not be confirmed. Run ignotum deploy --resume ${deploymentId} to finish activation.`
32789
+ }) : cause;
32453
32790
  const replaceBuildDirectory = Effect.fn("Deploy.replaceBuildDirectory")(function* (stateDirectory, stagingDirectory, buildDirectory) {
32454
32791
  const fileSystem = yield* FileSystem.FileSystem;
32455
32792
  if (!(yield* fileSystem.exists(buildDirectory))) {
@@ -32532,11 +32869,7 @@ const uploadDeployment = Effect.fn("Deploy.upload")(function* (configuration, bu
32532
32869
  deploymentId: deployment.id,
32533
32870
  message: `Deployment ${deployment.id} could not be finalized. The active deployment was not changed.`
32534
32871
  }) }));
32535
- yield* control.activateDeployment(configuration.appId, deployment.id).pipe(Effect.catchTags({ ControlClientUnavailable: (cause) => DeploymentActivationUnconfirmed.make({
32536
- cause,
32537
- deploymentId: deployment.id,
32538
- message: `Deployment ${deployment.id} is ready, but activation could not be confirmed.`
32539
- }) }));
32872
+ yield* control.activateDeployment(configuration.appId, deployment.id).pipe(Effect.mapError((cause) => activationError(deployment.id, cause)));
32540
32873
  return deployment.id;
32541
32874
  });
32542
32875
  const deploy$1 = Effect.fn("Deploy.run")(function* (appDirectory, requestedSlug) {
@@ -32549,6 +32882,14 @@ const deploy$1 = Effect.fn("Deploy.run")(function* (appDirectory, requestedSlug)
32549
32882
  deploymentId
32550
32883
  };
32551
32884
  });
32885
+ const resumeDeployment = Effect.fn("Deploy.resume")(function* (appDirectory, deploymentId, requestedSlug) {
32886
+ const configuration = yield* configureApp(appDirectory, requestedSlug);
32887
+ yield* (yield* ControlClient).activateDeployment(configuration.appId, deploymentId).pipe(Effect.mapError((cause) => activationError(deploymentId, cause)));
32888
+ return {
32889
+ appUrl: appUrl(configuration.slug),
32890
+ deploymentId
32891
+ };
32892
+ });
32552
32893
  //#endregion
32553
32894
  //#region src/dev-runtime/dev-database.ts
32554
32895
  const databaseFileNames = [
@@ -32681,195 +33022,6 @@ const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (appDirectory)
32681
33022
  }));
32682
33023
  });
32683
33024
  //#endregion
32684
- //#region ../../node_modules/.pnpm/@effect+sql-sqlite-node@4.0.0-rc.111_effect@4.0.0-rc.111/node_modules/@effect/sql-sqlite-node/dist/SqliteClient.js
32685
- /**
32686
- * Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
32687
- *
32688
- * This module opens a SQLite database and exposes it as both `SqliteClient` and
32689
- * the generic Effect SQL client. It serializes access through one connection,
32690
- * caches prepared statements, enables WAL mode unless disabled, and waits up
32691
- * to five seconds for busy databases by default. Explicit transactions on
32692
- * writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock
32693
- * upgrades, which serializes them behind other writers even when they only
32694
- * read. Clients opened with `readonly: true` are unaffected. Busy waits block
32695
- * the Node.js event loop because `node:sqlite` is synchronous. Database backup
32696
- * and extension loading are supported; streaming queries and `updateValues`
32697
- * are not.
32698
- *
32699
- * @since 4.0.0
32700
- */
32701
- const ATTR_DB_SYSTEM_NAME = "db.system.name";
32702
- const MAX_BUSY_TIMEOUT = 2147483647;
32703
- /**
32704
- * Runtime type identifier used to mark Node `SqliteClient` values.
32705
- *
32706
- * @category type IDs
32707
- * @since 4.0.0
32708
- */
32709
- const TypeId$1 = "~@effect/sql-sqlite-node/SqliteClient";
32710
- /**
32711
- * Service tag for the node SQLite client implementation.
32712
- *
32713
- * @category services
32714
- * @since 4.0.0
32715
- */
32716
- const SqliteClient = /*#__PURE__*/ Context$1.Service("@effect/sql-sqlite-node/SqliteClient");
32717
- /**
32718
- * Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL and a 5-second busy timeout enabled by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected.
32719
- *
32720
- * @category constructors
32721
- * @since 4.0.0
32722
- */
32723
- const make$2 = (options) => Effect$1.gen(function* () {
32724
- const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
32725
- const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : void 0;
32726
- const makeConnection = Effect$1.gen(function* () {
32727
- const scope = yield* Effect$1.scope;
32728
- const db = new DatabaseSync(options.filename, {
32729
- readOnly: options.readonly ?? false,
32730
- allowExtension: true
32731
- });
32732
- yield* Scope.addFinalizer(scope, Effect$1.sync(() => db.close()));
32733
- db.enableLoadExtension(false);
32734
- const busyTimeout = Math.min(MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))));
32735
- db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
32736
- if (options.disableWAL !== true) db.exec("PRAGMA journal_mode = WAL");
32737
- const prepareCache = yield* Cache.make({
32738
- capacity: options.prepareCacheSize ?? 200,
32739
- timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
32740
- lookup: (sql) => Effect$1.try({
32741
- try: () => db.prepare(sql),
32742
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
32743
- })
32744
- });
32745
- const runStatement = (statement, params, raw) => Effect$1.withFiber((fiber) => {
32746
- const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
32747
- return Effect$1.try({
32748
- try: () => {
32749
- statement.setReadBigInts(useSafeIntegers);
32750
- if (statement.columns().length > 0) return statement.all(...params);
32751
- const result = statement.run(...params);
32752
- return raw ? {
32753
- changes: result.changes,
32754
- lastInsertRowid: result.lastInsertRowid
32755
- } : [];
32756
- },
32757
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
32758
- });
32759
- });
32760
- const runStatementValues = (statement, params) => Effect$1.withFiber((fiber) => {
32761
- const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
32762
- return Effect$1.try({
32763
- try: () => {
32764
- statement.setReadBigInts(useSafeIntegers);
32765
- if (statement.columns().length > 0) return statement.all(...params);
32766
- statement.run(...params);
32767
- return [];
32768
- },
32769
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
32770
- });
32771
- });
32772
- const runStatementValuesUnprepared = (statement, params) => Effect$1.withFiber((fiber) => {
32773
- const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
32774
- return Effect$1.try({
32775
- try: () => {
32776
- statement.setReadBigInts(useSafeIntegers);
32777
- statement.setReturnArrays(true);
32778
- if (statement.columns().length > 0) return statement.all(...params);
32779
- statement.run(...params);
32780
- return [];
32781
- },
32782
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
32783
- });
32784
- });
32785
- const run = (sql, params, raw = false) => Effect$1.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
32786
- const runValues = (sql, params) => Effect$1.acquireUseRelease(Cache.get(prepareCache, sql), (statement) => {
32787
- statement.setReturnArrays(true);
32788
- return runStatementValues(statement, params);
32789
- }, (statement) => Effect$1.sync(() => statement.setReturnArrays(false)));
32790
- const runValuesUnprepared = (sql, params) => runStatementValuesUnprepared(db.prepare(sql), params);
32791
- return identity({
32792
- execute(sql, params, transformRows) {
32793
- return transformRows ? Effect$1.map(run(sql, params), transformRows) : run(sql, params);
32794
- },
32795
- executeRaw(sql, params) {
32796
- return run(sql, params, true);
32797
- },
32798
- executeValues(sql, params) {
32799
- return runValues(sql, params);
32800
- },
32801
- executeValuesUnprepared(sql, params) {
32802
- return runValuesUnprepared(sql, params);
32803
- },
32804
- executeUnprepared(sql, params, transformRows) {
32805
- const effect = runStatement(db.prepare(sql), params ?? [], false);
32806
- return transformRows ? Effect$1.map(effect, transformRows) : effect;
32807
- },
32808
- executeStream(_sql, _params) {
32809
- return Stream$1.die("executeStream not implemented");
32810
- },
32811
- backup(destination) {
32812
- return Effect$1.suspend(() => {
32813
- let totalPages = 0;
32814
- return Effect$1.tryPromise({
32815
- try: () => backup(db, destination, { progress: (progress) => {
32816
- totalPages = progress.totalPages;
32817
- } }).then((pages) => ({
32818
- totalPages: totalPages || pages,
32819
- remainingPages: 0
32820
- })),
32821
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
32822
- });
32823
- });
32824
- },
32825
- loadExtension(path) {
32826
- return Effect$1.acquireUseRelease(Effect$1.sync(() => db.enableLoadExtension(true)), () => Effect$1.try({
32827
- try: () => db.loadExtension(path),
32828
- catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
32829
- }), () => Effect$1.sync(() => db.enableLoadExtension(false)));
32830
- }
32831
- });
32832
- });
32833
- const semaphore = yield* Semaphore$1.make(1);
32834
- const connection = yield* makeConnection;
32835
- const acquirer = semaphore.withPermits(1)(Effect$1.succeed(connection));
32836
- const transactionAcquirer = Effect$1.uninterruptibleMask((restore) => {
32837
- const fiber = Fiber.getCurrent();
32838
- const scope = Context$1.getUnsafe(fiber.context, Scope.Scope);
32839
- return Effect$1.as(Effect$1.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection);
32840
- });
32841
- return Object.assign(yield* Client.make({
32842
- acquirer,
32843
- compiler,
32844
- transactionAcquirer,
32845
- beginTransaction: "BEGIN IMMEDIATE",
32846
- spanAttributes: [...options.spanAttributes ? Object.entries(options.spanAttributes) : [], [ATTR_DB_SYSTEM_NAME, "sqlite"]],
32847
- transformRows
32848
- }), {
32849
- [TypeId$1]: TypeId$1,
32850
- config: options,
32851
- backup: (destination) => Effect$1.flatMap(acquirer, (_) => _.backup(destination)),
32852
- loadExtension: (path) => Effect$1.flatMap(acquirer, (_) => _.loadExtension(path))
32853
- });
32854
- });
32855
- /**
32856
- * Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
32857
- *
32858
- * @category layers
32859
- * @since 4.0.0
32860
- */
32861
- const layer = (config) => Layer$1.effectContext(Effect$1.map(make$2(config), (client) => Context$1.make(SqliteClient, client).pipe(Context$1.add(Client.SqlClient, client)))).pipe(Layer$1.provide(Reactivity.layer));
32862
- const classifyError = (cause, message, operation) => classifySqliteError(sqliteCauseWithErrno(cause), {
32863
- message,
32864
- operation
32865
- });
32866
- const sqliteCauseWithErrno = (cause) => {
32867
- if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) return cause;
32868
- const errcode = cause.errcode;
32869
- if (typeof errcode !== "number") return cause;
32870
- return Object.assign(cause, { errno: errcode });
32871
- };
32872
- //#endregion
32873
33025
  //#region ../contracts/dist/runtime/index.js
32874
33026
  const IndexIdentity = Schema.String.pipe(Schema.brand("ignotum/runtime/IndexIdentity"));
32875
33027
  const EncodedIndexKey = Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"));
@@ -32903,6 +33055,7 @@ const scalarKind = (descriptor) => {
32903
33055
  case "date":
32904
33056
  case "integer":
32905
33057
  case "number": return "number";
33058
+ case "userId":
32906
33059
  case "id":
32907
33060
  case "string": return "string";
32908
33061
  case "literal": return scalarValueKind(descriptor.value);
@@ -33001,7 +33154,9 @@ const IndexPointInvalidation = Schema.Struct({
33001
33154
  index: IndexIdentity,
33002
33155
  key: EncodedIndexKey
33003
33156
  });
33157
+ const IdDependency = Schema.Struct({ type: Schema.Literal("Id") });
33004
33158
  const ReadDependency = Schema.Union([
33159
+ IdDependency,
33005
33160
  TableDependency,
33006
33161
  DocumentDependency,
33007
33162
  IndexRangeDependency
@@ -33066,7 +33221,8 @@ const InvocationBase = {
33066
33221
  generation: DeploymentGeneration,
33067
33222
  invocationKey: InvocationKey,
33068
33223
  function: FunctionAddress,
33069
- args: Schema.Json
33224
+ args: Schema.Json,
33225
+ identity: Schema.optional(SessionState)
33070
33226
  };
33071
33227
  const QueryInvocation = Schema.Struct({
33072
33228
  ...InvocationBase,
@@ -33089,6 +33245,7 @@ var RuntimeDeploymentUnavailable = class extends Schema.TaggedError()("RuntimeDe
33089
33245
  message: Schema.String
33090
33246
  }) {};
33091
33247
  const RuntimeInvocationRejectionCode = Schema.Literals([
33248
+ "SessionExpired",
33092
33249
  "FunctionUnavailable",
33093
33250
  "InvalidArguments",
33094
33251
  "InvocationIdConflict",
@@ -33114,7 +33271,7 @@ Schema.Union([
33114
33271
  RuntimeInvocationRejected,
33115
33272
  RuntimeInvocationUnavailable
33116
33273
  ]);
33117
- const dependencyKey = (dependency) => dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
33274
+ const dependencyKey = (dependency) => dependency.type === "Id" ? "Id" : dependency.type === "Table" ? encodeCanonicalJson([dependency.type, dependency.tableId]) : dependency.type === "Document" ? encodeCanonicalJson([
33118
33275
  dependency.type,
33119
33276
  dependency.tableId,
33120
33277
  dependency.id
@@ -33133,11 +33290,17 @@ const canonicalQueryKey = (deploymentId, functionAddress, args) => QueryKey.make
33133
33290
  functionAddress,
33134
33291
  args
33135
33292
  ]));
33136
- const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args) => encodeCanonicalJson([
33293
+ const canonicalInvocationInput = (deploymentId, functionKind, functionAddress, args, sessionEpoch) => encodeCanonicalJson(sessionEpoch === void 0 ? [
33137
33294
  deploymentId,
33138
33295
  functionKind,
33139
33296
  functionAddress,
33140
33297
  args
33298
+ ] : [
33299
+ deploymentId,
33300
+ functionKind,
33301
+ functionAddress,
33302
+ args,
33303
+ sessionEpoch
33141
33304
  ]);
33142
33305
  const tableDependency = (tableId) => ({
33143
33306
  type: "Table",
@@ -33162,68 +33325,6 @@ const indexPointInvalidation = (tableId, index, key) => ({
33162
33325
  key
33163
33326
  });
33164
33327
  //#endregion
33165
- //#region ../runtime/dist/functions.js
33166
- var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
33167
- //#endregion
33168
- //#region ../contracts/dist/runtime/transport.js
33169
- const runtimeInvocationPath = "/v1/invoke";
33170
- const runtimeRevisionPath = "/v1/revision";
33171
- const runtimeIndexPreparePath = "/v1/indexes/prepare";
33172
- const runtimeIndexCommitPath = "/v1/indexes/commit";
33173
- const runtimeFileEffectsPath = "/v1/files/effects";
33174
- const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
33175
- const ignotumPathPrefix$1 = "/_ignotum";
33176
- const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
33177
- const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
33178
- const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
33179
- const RuntimeRequestPath = Schema.Literals([
33180
- runtimeInvocationPath,
33181
- runtimeRevisionPath,
33182
- runtimeIndexPreparePath,
33183
- runtimeIndexCommitPath,
33184
- runtimeFileEffectsPath,
33185
- runtimeFileEffectsAcknowledgePath
33186
- ]);
33187
- const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
33188
- const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
33189
- Schema.Struct({
33190
- appId: AppId,
33191
- deploymentId: Schema.optional(DeploymentId),
33192
- nonce: RuntimeRequestNonce,
33193
- timestamp: RuntimeRequestTimestamp,
33194
- signature: RuntimeRequestSignature
33195
- });
33196
- Schema.Struct({
33197
- method: Schema.Literal("POST"),
33198
- path: RuntimeRequestPath,
33199
- timestamp: RuntimeRequestTimestamp,
33200
- nonce: RuntimeRequestNonce,
33201
- appId: AppId,
33202
- deploymentId: Schema.optional(DeploymentId),
33203
- bodySha256: Sha256
33204
- });
33205
- Schema.Struct({
33206
- appId: AppId,
33207
- deploymentId: DeploymentId
33208
- });
33209
- Schema.Struct({ completed: Schema.Literal(true) });
33210
- const RuntimeErrorCode = Schema.Literals([
33211
- "InvalidRequest",
33212
- "InvalidSignature",
33213
- "RequestExpired",
33214
- "RequestReplay",
33215
- "InvocationRejected",
33216
- "RuntimeUnavailable"
33217
- ]);
33218
- Schema.Struct({
33219
- code: RuntimeErrorCode,
33220
- message: Schema.String,
33221
- rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
33222
- });
33223
- //#endregion
33224
- //#region ../shared/dist/http-body.js
33225
- const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => stream.pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.mkUint8Array));
33226
- //#endregion
33227
33328
  //#region ../runtime/dist/sync.js
33228
33329
  const makeDependencyIndex = () => {
33229
33330
  const dependenciesByQuery = MutableHashMap.empty();
@@ -33294,6 +33395,470 @@ const makeDependencyIndex = () => {
33294
33395
  };
33295
33396
  };
33296
33397
  var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
33398
+ //#endregion
33399
+ //#region src/dev-runtime/query-cache.ts
33400
+ var LocalQueryCache = class LocalQueryCache extends Context.Service()("ignotum/dev-runtime/query-cache/LocalQueryCache") {
33401
+ static layer = Layer.effect(LocalQueryCache, Effect.sync(() => {
33402
+ const results = /* @__PURE__ */ new Map();
33403
+ const dependencies = makeDependencyIndex();
33404
+ const flights = /* @__PURE__ */ new Map();
33405
+ const publicQueries = /* @__PURE__ */ new Set();
33406
+ const locks = /* @__PURE__ */ new Map();
33407
+ let generation = 0;
33408
+ const save = (key, result) => {
33409
+ results.delete(key);
33410
+ results.set(key, result);
33411
+ dependencies.record(key, result.dependencies);
33412
+ while (results.size > 128) {
33413
+ const oldest = results.keys().next().value;
33414
+ if (oldest === void 0) break;
33415
+ results.delete(oldest);
33416
+ dependencies.remove(oldest);
33417
+ publicQueries.delete(oldest);
33418
+ }
33419
+ };
33420
+ return LocalQueryCache.of({
33421
+ invalidate: (event) => {
33422
+ generation++;
33423
+ const keys = event.type === "All" ? [...results.keys()] : [...dependencies.affected(event.invalidations)];
33424
+ for (const key of keys) {
33425
+ results.delete(key);
33426
+ dependencies.remove(key);
33427
+ }
33428
+ if (event.type === "All") publicQueries.clear();
33429
+ },
33430
+ execute: Effect.fn("LocalQueryCache.execute")(function* (base, identity, minimumRevision, execute) {
33431
+ const admitted = generation;
33432
+ const scoped = `${base}:id:${idViewKey(identity)}`;
33433
+ const cached = results.get(base) ?? results.get(scoped);
33434
+ if (cached !== void 0 && cached.observedRevision >= minimumRevision) return cached;
33435
+ const key = `${scoped}:${admitted}`;
33436
+ const pending = flights.get(key);
33437
+ if (pending !== void 0) return yield* Deferred.await(pending);
33438
+ const done = yield* Deferred.make();
33439
+ flights.set(key, done);
33440
+ const evaluate = Effect.gen(function* () {
33441
+ const shared = results.get(base);
33442
+ if (shared !== void 0 && shared.observedRevision >= minimumRevision) return shared;
33443
+ const result = yield* execute;
33444
+ if (result.type === "Query" && generation === admitted) {
33445
+ const readsId = result.dependencies.some((dependency) => dependency.type === "Id");
33446
+ save(scoped, result);
33447
+ if (!readsId) {
33448
+ publicQueries.add(base);
33449
+ save(base, result);
33450
+ }
33451
+ }
33452
+ return result;
33453
+ });
33454
+ const lockKey = `${base}:${admitted}`;
33455
+ let lock = locks.get(lockKey);
33456
+ if (publicQueries.has(base) && lock === void 0) {
33457
+ lock = Semaphore.makeUnsafe(1);
33458
+ locks.set(lockKey, lock);
33459
+ }
33460
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
33461
+ const exit = yield* restore(lock === void 0 ? evaluate : lock.withPermits(1)(evaluate)).pipe(Effect.exit);
33462
+ Deferred.doneUnsafe(done, Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause));
33463
+ flights.delete(key);
33464
+ locks.delete(lockKey);
33465
+ return yield* exit;
33466
+ }));
33467
+ })
33468
+ });
33469
+ }));
33470
+ };
33471
+ //#endregion
33472
+ //#region src/dev-runtime/id.ts
33473
+ const Profile = Schema.Struct({
33474
+ user: User,
33475
+ share: ProfileFields,
33476
+ revision: Schema.Natural
33477
+ });
33478
+ const Profiles = initial(Schema.Struct({
33479
+ formatVersion: Schema.Literal(1),
33480
+ profiles: Schema.Record(Schema.String, Profile)
33481
+ }));
33482
+ const ProfilesJson = Schema.fromJsonString(Profiles);
33483
+ const localIdPath = "/_ignotum/v1/id";
33484
+ const LocalCompletion = Schema.Struct({
33485
+ request: SessionRequest,
33486
+ username: DevelopmentUsername,
33487
+ name: Schema.String,
33488
+ email: Schema.String,
33489
+ image: Schema.String,
33490
+ share: ProfileFields,
33491
+ manage: Schema.optional(Schema.Boolean)
33492
+ });
33493
+ const projection = (user, share) => ({
33494
+ id: user.id,
33495
+ ...Record.filter({
33496
+ name: share.includes("name") ? user.name : void 0,
33497
+ email: share.includes("email") ? user.email : void 0,
33498
+ image: share.includes("image") ? user.image : void 0
33499
+ }, Predicate.isNotUndefined)
33500
+ });
33501
+ var LocalId = class LocalId extends Context.Service()("ignotum/dev-runtime/id/LocalId") {
33502
+ static layer = (directory) => Layer.effect(LocalId, Effect.gen(function* () {
33503
+ const fs = yield* FileSystem.FileSystem;
33504
+ const path = yield* Path$1.Path;
33505
+ const file = path.join(directory, ".ignotum", "id.json");
33506
+ const lock = yield* Semaphore.make(1);
33507
+ const load = Effect.fn("LocalId.load")(function* () {
33508
+ if (!(yield* fs.exists(file))) return Profiles.make({
33509
+ formatVersion: 1,
33510
+ profiles: {}
33511
+ });
33512
+ return yield* fs.readFileString(file).pipe(Effect.flatMap(Schema.decodeEffect(ProfilesJson)));
33513
+ });
33514
+ return LocalId.of({
33515
+ profile: Effect.fn("LocalId.profile")(function* (username) {
33516
+ return (yield* load().pipe(Effect.orDie)).profiles[username];
33517
+ }),
33518
+ session: Effect.fn("LocalId.session")(function* (username, epoch) {
33519
+ if (username === null) return {
33520
+ sessionEpoch: epoch,
33521
+ viewRevision: 0,
33522
+ user: null,
33523
+ validUntil: Number.MAX_SAFE_INTEGER
33524
+ };
33525
+ const id = yield* Schema.decodeEffect(UserId)(username).pipe(Effect.orDie);
33526
+ const profile = (yield* load().pipe(Effect.orDie)).profiles[id];
33527
+ const user = projection(profile?.user ?? { id }, profile?.share ?? []);
33528
+ return {
33529
+ sessionEpoch: epoch,
33530
+ viewRevision: profile?.revision ?? 0,
33531
+ user,
33532
+ validUntil: Number.MAX_SAFE_INTEGER
33533
+ };
33534
+ }),
33535
+ complete: Effect.fn("LocalId.complete")(function* (input) {
33536
+ yield* lock.withPermits(1)(Effect.gen(function* () {
33537
+ const stored = yield* load();
33538
+ const previous = stored.profiles[input.username];
33539
+ const user = User.make({
33540
+ id: UserId.make(input.username),
33541
+ name: input.name === "" ? void 0 : input.name,
33542
+ email: input.email === "" ? void 0 : input.email,
33543
+ image: input.image === "" ? void 0 : input.image
33544
+ });
33545
+ const share = input.manage === true ? input.share : [.../* @__PURE__ */ new Set([...previous?.share ?? [], ...input.share.filter((field) => input.request.profile.includes(field))])];
33546
+ const updated = Profiles.make({
33547
+ formatVersion: 1,
33548
+ profiles: {
33549
+ ...stored.profiles,
33550
+ [input.username]: {
33551
+ user,
33552
+ share,
33553
+ revision: (previous?.revision ?? 0) + Number(JSON.stringify(projection(previous?.user ?? { id: user.id }, previous?.share ?? [])) !== JSON.stringify(projection(user, share)))
33554
+ }
33555
+ }
33556
+ });
33557
+ yield* fs.makeDirectory(path.dirname(file), { recursive: true });
33558
+ yield* Effect.scoped(Effect.gen(function* () {
33559
+ const temporary = yield* fs.makeTempFileScoped({
33560
+ directory: path.dirname(file),
33561
+ prefix: ".id-"
33562
+ });
33563
+ yield* fs.writeFileString(temporary, yield* Schema.encodeEffect(ProfilesJson)(updated));
33564
+ yield* fs.rename(temporary, file);
33565
+ }));
33566
+ })).pipe(Effect.orDie);
33567
+ })
33568
+ });
33569
+ }));
33570
+ };
33571
+ const escapeHtml = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;");
33572
+ const localIdPage = (request) => {
33573
+ const encoded = JSON.stringify(request).replaceAll("<", "\\u003c");
33574
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Ignotum ID · Development</title>
33575
+ <meta name="viewport" content="width=device-width,initial-scale=1"><style>
33576
+ body{font:16px system-ui;background:#f7f7f5;color:#20211f;max-width:420px;margin:8vh auto;padding:24px}
33577
+ label{display:block;margin:16px 0}input:not([type=checkbox]){box-sizing:border-box;width:100%;padding:10px;border:1px solid #bbb;border-radius:6px}
33578
+ button{padding:12px 18px;border:0;border-radius:6px;background:#222;color:white;cursor:pointer;margin-right:8px}
33579
+ p{line-height:1.5}small{color:#666}</style></head><body><h1>Ignotum ID</h1><p>Choose a local username to try your app.</p>
33580
+ <form id="form"><label>Username<input name="username" required pattern="[a-z][a-z0-9_-]{0,63}" placeholder="john"></label>
33581
+ <label>Name<input name="name"></label><label>Email<input name="email" type="email"></label><label>Image URL<input name="image" type="url"></label>
33582
+ ${[
33583
+ "name",
33584
+ "email",
33585
+ "image"
33586
+ ].map((field) => `<label><input type="checkbox" name="share" value="${escapeHtml(field)}"> Share ${escapeHtml(field)}</label>`).join("")}
33587
+ <label><input type="checkbox" name="manage"> Replace existing sharing choices</label>
33588
+ <p><small>Profile sharing is optional. No password or account is needed in development.</small></p>
33589
+ <button>Continue</button><button type="button" id="cancel">Cancel</button><p id="error"></p></form>
33590
+ <script type="module">
33591
+ const request=${encoded};const form=document.querySelector('#form');
33592
+ const previous=JSON.parse(sessionStorage.getItem('${developmentSelectionKey}')||'null');
33593
+ if(previous?.username)form.elements.username.value=previous.username;
33594
+ if(request.intent==='requestProfile')form.elements.username.readOnly=true;
33595
+ let loading=false;const loadProfile=async()=>{loading=true;try{const response=await fetch('${localIdPath}/profile?username='+encodeURIComponent(form.elements.username.value),{headers:{'x-ignotum-request':'1'}});if(!response.ok)throw new Error('Could not load the local profile.');const profile=await response.json();for(const field of ['name','email','image']){form.elements[field].value=profile?.user[field]??'';form.querySelector('input[name=share][value='+field+']').checked=profile?.share.includes(field)??false;}}finally{loading=false;}};form.elements.username.onchange=loadProfile;if(previous?.username)await loadProfile();
33596
+ document.querySelector('#cancel').onclick=()=>{sessionStorage.setItem('ignotum.id.outcome','Cancelled');location.replace(request.returnTo)};
33597
+ form.onsubmit=async(e)=>{e.preventDefault();if(loading)return;const data=new FormData(form);
33598
+ try{const response=await fetch('${localIdPath}',{method:'POST',headers:{'content-type':'application/json','x-ignotum-request':'1'},body:JSON.stringify({request,username:data.get('username'),name:data.get('name'),email:data.get('email'),image:data.get('image'),share:data.getAll('share'),manage:data.get('manage')==='on'})});
33599
+ if(!response.ok)throw new Error('Could not update your local ID.');
33600
+ sessionStorage.setItem('${developmentSelectionKey}',JSON.stringify({formatVersion:1,username:data.get('username'),epoch:request.intent==='requestProfile'?previous.epoch:'dev:'+crypto.randomUUID()}));
33601
+ sessionStorage.setItem('ignotum.id.outcome',request.intent==='requestProfile'?'ProfileUpdated':'SignedIn');location.replace(request.returnTo);
33602
+ }catch(error){document.querySelector('#error').textContent=error.message}}
33603
+ <\/script></body></html>`;
33604
+ };
33605
+ //#endregion
33606
+ //#region ../../node_modules/.pnpm/@effect+sql-sqlite-node@4.0.0-rc.111_effect@4.0.0-rc.111/node_modules/@effect/sql-sqlite-node/dist/SqliteClient.js
33607
+ /**
33608
+ * Connects Effect SQL to SQLite on Node.js using `node:sqlite`.
33609
+ *
33610
+ * This module opens a SQLite database and exposes it as both `SqliteClient` and
33611
+ * the generic Effect SQL client. It serializes access through one connection,
33612
+ * caches prepared statements, enables WAL mode unless disabled, and waits up
33613
+ * to five seconds for busy databases by default. Explicit transactions on
33614
+ * writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock
33615
+ * upgrades, which serializes them behind other writers even when they only
33616
+ * read. Clients opened with `readonly: true` are unaffected. Busy waits block
33617
+ * the Node.js event loop because `node:sqlite` is synchronous. Database backup
33618
+ * and extension loading are supported; streaming queries and `updateValues`
33619
+ * are not.
33620
+ *
33621
+ * @since 4.0.0
33622
+ */
33623
+ const ATTR_DB_SYSTEM_NAME = "db.system.name";
33624
+ const MAX_BUSY_TIMEOUT = 2147483647;
33625
+ /**
33626
+ * Runtime type identifier used to mark Node `SqliteClient` values.
33627
+ *
33628
+ * @category type IDs
33629
+ * @since 4.0.0
33630
+ */
33631
+ const TypeId$1 = "~@effect/sql-sqlite-node/SqliteClient";
33632
+ /**
33633
+ * Service tag for the node SQLite client implementation.
33634
+ *
33635
+ * @category services
33636
+ * @since 4.0.0
33637
+ */
33638
+ const SqliteClient = /*#__PURE__*/ Context$1.Service("@effect/sql-sqlite-node/SqliteClient");
33639
+ /**
33640
+ * Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL and a 5-second busy timeout enabled by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected.
33641
+ *
33642
+ * @category constructors
33643
+ * @since 4.0.0
33644
+ */
33645
+ const make$2 = (options) => Effect$1.gen(function* () {
33646
+ const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
33647
+ const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : void 0;
33648
+ const makeConnection = Effect$1.gen(function* () {
33649
+ const scope = yield* Effect$1.scope;
33650
+ const db = new DatabaseSync(options.filename, {
33651
+ readOnly: options.readonly ?? false,
33652
+ allowExtension: true
33653
+ });
33654
+ yield* Scope.addFinalizer(scope, Effect$1.sync(() => db.close()));
33655
+ db.enableLoadExtension(false);
33656
+ const busyTimeout = Math.min(MAX_BUSY_TIMEOUT, Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))));
33657
+ db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
33658
+ if (options.disableWAL !== true) db.exec("PRAGMA journal_mode = WAL");
33659
+ const prepareCache = yield* Cache.make({
33660
+ capacity: options.prepareCacheSize ?? 200,
33661
+ timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
33662
+ lookup: (sql) => Effect$1.try({
33663
+ try: () => db.prepare(sql),
33664
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to prepare statement", "prepare") })
33665
+ })
33666
+ });
33667
+ const runStatement = (statement, params, raw) => Effect$1.withFiber((fiber) => {
33668
+ const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
33669
+ return Effect$1.try({
33670
+ try: () => {
33671
+ statement.setReadBigInts(useSafeIntegers);
33672
+ if (statement.columns().length > 0) return statement.all(...params);
33673
+ const result = statement.run(...params);
33674
+ return raw ? {
33675
+ changes: result.changes,
33676
+ lastInsertRowid: result.lastInsertRowid
33677
+ } : [];
33678
+ },
33679
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
33680
+ });
33681
+ });
33682
+ const runStatementValues = (statement, params) => Effect$1.withFiber((fiber) => {
33683
+ const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
33684
+ return Effect$1.try({
33685
+ try: () => {
33686
+ statement.setReadBigInts(useSafeIntegers);
33687
+ if (statement.columns().length > 0) return statement.all(...params);
33688
+ statement.run(...params);
33689
+ return [];
33690
+ },
33691
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
33692
+ });
33693
+ });
33694
+ const runStatementValuesUnprepared = (statement, params) => Effect$1.withFiber((fiber) => {
33695
+ const useSafeIntegers = Context$1.get(fiber.context, Client.SafeIntegers);
33696
+ return Effect$1.try({
33697
+ try: () => {
33698
+ statement.setReadBigInts(useSafeIntegers);
33699
+ statement.setReturnArrays(true);
33700
+ if (statement.columns().length > 0) return statement.all(...params);
33701
+ statement.run(...params);
33702
+ return [];
33703
+ },
33704
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })
33705
+ });
33706
+ });
33707
+ const run = (sql, params, raw = false) => Effect$1.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
33708
+ const runValues = (sql, params) => Effect$1.acquireUseRelease(Cache.get(prepareCache, sql), (statement) => {
33709
+ statement.setReturnArrays(true);
33710
+ return runStatementValues(statement, params);
33711
+ }, (statement) => Effect$1.sync(() => statement.setReturnArrays(false)));
33712
+ const runValuesUnprepared = (sql, params) => runStatementValuesUnprepared(db.prepare(sql), params);
33713
+ return identity({
33714
+ execute(sql, params, transformRows) {
33715
+ return transformRows ? Effect$1.map(run(sql, params), transformRows) : run(sql, params);
33716
+ },
33717
+ executeRaw(sql, params) {
33718
+ return run(sql, params, true);
33719
+ },
33720
+ executeValues(sql, params) {
33721
+ return runValues(sql, params);
33722
+ },
33723
+ executeValuesUnprepared(sql, params) {
33724
+ return runValuesUnprepared(sql, params);
33725
+ },
33726
+ executeUnprepared(sql, params, transformRows) {
33727
+ const effect = runStatement(db.prepare(sql), params ?? [], false);
33728
+ return transformRows ? Effect$1.map(effect, transformRows) : effect;
33729
+ },
33730
+ executeStream(_sql, _params) {
33731
+ return Stream$1.die("executeStream not implemented");
33732
+ },
33733
+ backup(destination) {
33734
+ return Effect$1.suspend(() => {
33735
+ let totalPages = 0;
33736
+ return Effect$1.tryPromise({
33737
+ try: () => backup(db, destination, { progress: (progress) => {
33738
+ totalPages = progress.totalPages;
33739
+ } }).then((pages) => ({
33740
+ totalPages: totalPages || pages,
33741
+ remainingPages: 0
33742
+ })),
33743
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to backup database", "backup") })
33744
+ });
33745
+ });
33746
+ },
33747
+ loadExtension(path) {
33748
+ return Effect$1.acquireUseRelease(Effect$1.sync(() => db.enableLoadExtension(true)), () => Effect$1.try({
33749
+ try: () => db.loadExtension(path),
33750
+ catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") })
33751
+ }), () => Effect$1.sync(() => db.enableLoadExtension(false)));
33752
+ }
33753
+ });
33754
+ });
33755
+ const semaphore = yield* Semaphore$1.make(1);
33756
+ const connection = yield* makeConnection;
33757
+ const acquirer = semaphore.withPermits(1)(Effect$1.succeed(connection));
33758
+ const transactionAcquirer = Effect$1.uninterruptibleMask((restore) => {
33759
+ const fiber = Fiber.getCurrent();
33760
+ const scope = Context$1.getUnsafe(fiber.context, Scope.Scope);
33761
+ return Effect$1.as(Effect$1.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), connection);
33762
+ });
33763
+ return Object.assign(yield* Client.make({
33764
+ acquirer,
33765
+ compiler,
33766
+ transactionAcquirer,
33767
+ beginTransaction: "BEGIN IMMEDIATE",
33768
+ spanAttributes: [...options.spanAttributes ? Object.entries(options.spanAttributes) : [], [ATTR_DB_SYSTEM_NAME, "sqlite"]],
33769
+ transformRows
33770
+ }), {
33771
+ [TypeId$1]: TypeId$1,
33772
+ config: options,
33773
+ backup: (destination) => Effect$1.flatMap(acquirer, (_) => _.backup(destination)),
33774
+ loadExtension: (path) => Effect$1.flatMap(acquirer, (_) => _.loadExtension(path))
33775
+ });
33776
+ });
33777
+ /**
33778
+ * Builds a layer from a node SQLite client configuration, providing both `SqliteClient` and the generic `SqlClient` service.
33779
+ *
33780
+ * @category layers
33781
+ * @since 4.0.0
33782
+ */
33783
+ const layer = (config) => Layer$1.effectContext(Effect$1.map(make$2(config), (client) => Context$1.make(SqliteClient, client).pipe(Context$1.add(Client.SqlClient, client)))).pipe(Layer$1.provide(Reactivity.layer));
33784
+ const classifyError = (cause, message, operation) => classifySqliteError(sqliteCauseWithErrno(cause), {
33785
+ message,
33786
+ operation
33787
+ });
33788
+ const sqliteCauseWithErrno = (cause) => {
33789
+ if (typeof cause !== "object" || cause === null || !("errcode" in cause) || "errno" in cause) return cause;
33790
+ const errcode = cause.errcode;
33791
+ if (typeof errcode !== "number") return cause;
33792
+ return Object.assign(cause, { errno: errcode });
33793
+ };
33794
+ //#endregion
33795
+ //#region ../runtime/dist/functions.js
33796
+ var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
33797
+ //#endregion
33798
+ //#region ../contracts/dist/runtime/transport.js
33799
+ const runtimeInvocationPath = "/v1/invoke";
33800
+ const runtimeConsolePath = "/v1/console/read";
33801
+ const runtimeRevisionPath = "/v1/revision";
33802
+ const runtimeIndexPreparePath = "/v1/indexes/prepare";
33803
+ const runtimeIndexCommitPath = "/v1/indexes/commit";
33804
+ const runtimeFileEffectsPath = "/v1/files/effects";
33805
+ const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
33806
+ const ignotumPathPrefix$1 = "/_ignotum";
33807
+ const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
33808
+ const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
33809
+ const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
33810
+ const RuntimeRequestPath = Schema.Literals([
33811
+ runtimeConsolePath,
33812
+ runtimeInvocationPath,
33813
+ runtimeRevisionPath,
33814
+ runtimeIndexPreparePath,
33815
+ runtimeIndexCommitPath,
33816
+ runtimeFileEffectsPath,
33817
+ runtimeFileEffectsAcknowledgePath
33818
+ ]);
33819
+ const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
33820
+ const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
33821
+ Schema.Struct({
33822
+ appId: AppId,
33823
+ deploymentId: Schema.optional(DeploymentId),
33824
+ nonce: RuntimeRequestNonce,
33825
+ timestamp: RuntimeRequestTimestamp,
33826
+ signature: RuntimeRequestSignature
33827
+ });
33828
+ Schema.Struct({
33829
+ method: Schema.Literal("POST"),
33830
+ path: RuntimeRequestPath,
33831
+ timestamp: RuntimeRequestTimestamp,
33832
+ nonce: RuntimeRequestNonce,
33833
+ appId: AppId,
33834
+ deploymentId: Schema.optional(DeploymentId),
33835
+ bodySha256: Sha256
33836
+ });
33837
+ Schema.Struct({
33838
+ appId: AppId,
33839
+ deploymentId: DeploymentId,
33840
+ generation: Schema.optional(Schema.Natural)
33841
+ });
33842
+ Schema.Union([Schema.Struct({ completed: Schema.Literal(true) }), Schema.Struct({
33843
+ completed: Schema.Literal(false),
33844
+ message: Schema.String
33845
+ })]);
33846
+ const RuntimeErrorCode = Schema.Literals([
33847
+ "InvalidRequest",
33848
+ "InvalidSignature",
33849
+ "RequestExpired",
33850
+ "RequestReplay",
33851
+ "InvocationRejected",
33852
+ "RuntimeUnavailable"
33853
+ ]);
33854
+ Schema.Struct({
33855
+ code: RuntimeErrorCode,
33856
+ message: Schema.String,
33857
+ rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
33858
+ });
33859
+ //#endregion
33860
+ //#region ../shared/dist/http-body.js
33861
+ const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => stream.pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.mkUint8Array));
33297
33862
  const syncPath = appSyncPath;
33298
33863
  const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
33299
33864
  //#endregion
@@ -33380,10 +33945,17 @@ const addApplicationFiles = Effect.gen(function* () {
33380
33945
  ) STRICT
33381
33946
  `;
33382
33947
  });
33948
+ const addSchemaState = Effect.gen(function* () {
33949
+ yield* (yield* Client.SqlClient)`CREATE TABLE schema_state (
33950
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
33951
+ snapshot TEXT NOT NULL CHECK (json_valid(snapshot))
33952
+ ) STRICT`;
33953
+ });
33383
33954
  const developmentMigrationLoader = Migrator.fromRecord({
33384
33955
  "0001_initial_development_schema": initialDevelopmentSchema,
33385
33956
  "0002_application_indexes": addDevelopmentIndexes,
33386
- "0003_application_files": addApplicationFiles
33957
+ "0003_application_files": addApplicationFiles,
33958
+ "0004_schema_state": addSchemaState
33387
33959
  });
33388
33960
  var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
33389
33961
  static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
@@ -33467,7 +34039,11 @@ const tableFor = Effect.fn("LocalDatabase.tableFor")(function* (schema, tableNam
33467
34039
  });
33468
34040
  const fieldsCodec = (table) => Schema.fromJsonString(Schema.make(Schema.toCodecJson(table.schema).ast));
33469
34041
  const encodeFields = Effect.fn("LocalDatabase.encodeFields")(function* (table, tableName, value, operation, id) {
33470
- return yield* Schema.encodeEffect(fieldsCodec(table), { onExcessProperty: "error" })(value).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
34042
+ const normalized = yield* Effect.try({
34043
+ try: () => operation === "patch" ? value : applyValueDefaults(table.schema, value),
34044
+ catch: (cause) => schemaMismatch(tableName, id, operation, cause)
34045
+ });
34046
+ return yield* Schema.encodeUnknownEffect(fieldsCodec(table), { onExcessProperty: "error" })(normalized).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
33471
34047
  });
33472
34048
  const decodeFields = Effect.fn("LocalDatabase.decodeFields")(function* (table, tableName, row, operation) {
33473
34049
  return yield* Schema.decodeEffect(fieldsCodec(table), { onExcessProperty: "error" })(row.fields).pipe(Effect.mapError((cause) => schemaMismatch(tableName, row.id, operation, cause)));
@@ -33636,6 +34212,54 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
33636
34212
  yield* Effect.forEach(stored.filter((index) => !expected.has(index.identity)), (index) => sql`DELETE FROM indexes WHERE id = ${index.id}`);
33637
34213
  }
33638
34214
  });
34215
+ const prepareSchema = Effect.fn("LocalDatabase.prepareSchema")(function* (schema) {
34216
+ const definitions = schema[SchemaDefinitionTypeId];
34217
+ const snapshot = encodeCanonical(SchemaSnapshot, {
34218
+ formatVersion: 2,
34219
+ tables: Object.entries(definitions).sort(([left], [right]) => left.localeCompare(right)).map(([name, definition]) => ({
34220
+ name,
34221
+ fields: descriptorFields(definition.fields),
34222
+ indexes: Object.values(definition.indexes).sort((left, right) => left.name.localeCompare(right.name))
34223
+ }))
34224
+ });
34225
+ const current = yield* sql`SELECT snapshot FROM schema_state WHERE singleton = 1`;
34226
+ if (current[0]?.snapshot === snapshot) return;
34227
+ let changedData = false;
34228
+ const storedTables = yield* sql`SELECT id, name FROM tables`;
34229
+ for (const stored of storedTables) {
34230
+ if (Object.hasOwn(definitions, stored.name)) continue;
34231
+ if ((yield* sql`SELECT 1 FROM documents WHERE tableId = ${stored.id} LIMIT 1`).length > 0) return yield* Effect.die(`Cannot remove non-empty table '${stored.name}'.`);
34232
+ yield* sql`DELETE FROM tables WHERE id = ${stored.id}`;
34233
+ }
34234
+ for (const [name, definition] of Object.entries(definitions)) {
34235
+ const table = yield* resolveTable(schema, name);
34236
+ let cursor = "";
34237
+ let changed = false;
34238
+ while (true) {
34239
+ const rows = yield* sql`
34240
+ SELECT id, fields FROM documents WHERE tableId = ${table.id} AND id > ${cursor}
34241
+ ORDER BY id LIMIT 100
34242
+ `;
34243
+ if (rows.length === 0) break;
34244
+ for (const row of rows) {
34245
+ const fields = yield* Effect.try({
34246
+ try: () => JSON.stringify(evolveStoredFields(definition.schema, JSON.parse(row.fields))),
34247
+ catch: () => /* @__PURE__ */ new Error(`Document '${row.id}' in '${name}' does not match the new schema or its conversion rules.`)
34248
+ }).pipe(Effect.orDie);
34249
+ if (fields !== row.fields) {
34250
+ yield* sql`UPDATE documents SET fields = ${fields} WHERE tableId = ${table.id} AND id = ${row.id}`;
34251
+ changed = true;
34252
+ changedData = true;
34253
+ }
34254
+ cursor = row.id;
34255
+ }
34256
+ }
34257
+ if (changed) yield* sql`DELETE FROM indexes WHERE tableId = ${table.id}`;
34258
+ }
34259
+ yield* sql`INSERT INTO schema_state (singleton, snapshot) VALUES (1, ${snapshot})
34260
+ ON CONFLICT (singleton) DO UPDATE SET snapshot = excluded.snapshot`;
34261
+ if (current.length > 0 || changedData) yield* advanceRevision(void 0);
34262
+ });
33639
34263
  const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (table, row) {
33640
34264
  return (yield* resolvedIndexes(table)).map((index) => ({
33641
34265
  index,
@@ -33956,6 +34580,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
33956
34580
  });
33957
34581
  };
33958
34582
  const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
34583
+ yield* prepareSchema(schema).pipe(Effect.orDie);
33959
34584
  yield* prepareIndexes(schema).pipe(Effect.orDie);
33960
34585
  const dependencies = makeDependencyRecorder();
33961
34586
  const value = yield* use(Object.freeze({ db: makeReader(schema, dependencies) }));
@@ -33967,6 +34592,7 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
33967
34592
  };
33968
34593
  }));
33969
34594
  const trackedMutationTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
34595
+ yield* prepareSchema(schema).pipe(Effect.orDie);
33970
34596
  yield* prepareIndexes(schema).pipe(Effect.orDie);
33971
34597
  const invalidations = makeDependencyRecorder();
33972
34598
  const value = yield* use(Object.freeze({ db: makeWriter(schema, invalidations) }));
@@ -34103,9 +34729,15 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
34103
34729
  dates
34104
34730
  };
34105
34731
  }));
34106
- return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved) {
34732
+ return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved, identity) {
34733
+ let readId = false;
34734
+ const id = makeIdContext(Effect.sync(() => {
34735
+ readId = true;
34736
+ return identity?.user ?? null;
34737
+ }));
34107
34738
  const invoke = (context) => Effect.gen(() => resolved.definition.handler(Object.freeze({
34108
34739
  ...context,
34740
+ id,
34109
34741
  env: resolved.environment ?? Object.freeze({})
34110
34742
  }), resolved.args)).pipe(Effect.matchEffect({
34111
34743
  onFailure: (error) => encodeFailure(resolved.definition, error),
@@ -34116,7 +34748,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
34116
34748
  const result = {
34117
34749
  type: "Query",
34118
34750
  result: value,
34119
- dependencies,
34751
+ dependencies: readId ? [...dependencies, { type: "Id" }] : dependencies,
34120
34752
  observedRevision
34121
34753
  };
34122
34754
  return files.length === 0 ? result : {
@@ -34155,7 +34787,7 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
34155
34787
  return {
34156
34788
  type: "Query",
34157
34789
  result,
34158
- dependencies: [],
34790
+ dependencies: readId ? [{ type: "Id" }] : [],
34159
34791
  observedRevision: yield* database.revision
34160
34792
  };
34161
34793
  })));
@@ -34166,9 +34798,9 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
34166
34798
  const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function* () {
34167
34799
  const registry = yield* FunctionRegistry;
34168
34800
  const executor = yield* FunctionExecutor;
34169
- return FunctionRuntime.of({ prepare: Effect.fn("DevFunctionRuntime.prepare")(function* (functionAddress, kind, args) {
34801
+ return FunctionRuntime.of({ prepare: Effect.fn("DevFunctionRuntime.prepare")(function* (functionAddress, kind, args, identity) {
34170
34802
  const resolved = yield* registry.resolve(functionAddress, kind, args);
34171
- return { execute: executor.execute(functionAddress, kind, resolved) };
34803
+ return { execute: executor.execute(functionAddress, kind, resolved, identity) };
34172
34804
  }) });
34173
34805
  }));
34174
34806
  //#endregion
@@ -34429,17 +35061,19 @@ const localSyncIdentity = {
34429
35061
  };
34430
35062
  const reloadEnvironmentAndInvalidate = (reload, publish) => reload.pipe(Effect.ensuring(publish));
34431
35063
  const queryInvalidationLayer = Layer.effect(QueryInvalidation, Effect.gen(function* () {
35064
+ const cache = yield* LocalQueryCache;
34432
35065
  const pubsub = yield* PubSub.unbounded();
34433
35066
  const latestRevision = yield* Ref.make(AppStateRevision.make(0));
34434
35067
  return QueryInvalidation.of({
34435
35068
  latestRevision: Ref.get(latestRevision),
34436
35069
  publish: (event) => Effect.gen(function* () {
35070
+ cache.invalidate(event);
34437
35071
  if (event.type === "Dependencies") yield* Ref.update(latestRevision, (current) => AppStateRevision.make(Math.max(current, event.committedRevision)));
34438
35072
  yield* PubSub.publish(pubsub, event);
34439
35073
  }).pipe(Effect.asVoid),
34440
35074
  subscribe: PubSub.subscribe(pubsub)
34441
35075
  });
34442
- }));
35076
+ })).pipe(Layer.provideMerge(LocalQueryCache.layer));
34443
35077
  var InvocationIdConflict = class extends Schema.TaggedError()("InvocationIdConflict", { id: Schema.String }) {};
34444
35078
  const MutationReplayRow = Schema.Struct({
34445
35079
  input: Schema.String,
@@ -34495,10 +35129,17 @@ const syncError = (code, message, operation) => {
34495
35129
  operation
34496
35130
  };
34497
35131
  };
34498
- const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
35132
+ const runSession = Effect.fn("SyncServer.runSession")(function* (socket, initialIdentity, refreshIdentity) {
35133
+ let identity = initialIdentity ?? {
35134
+ sessionEpoch: "dev:anonymous",
35135
+ user: null,
35136
+ viewRevision: 0,
35137
+ validUntil: Number.MAX_SAFE_INTEGER
35138
+ };
34499
35139
  const runtime = yield* FunctionRuntime;
34500
35140
  const invalidation = yield* QueryInvalidation;
34501
35141
  const mutationReplay = yield* MutationReplay;
35142
+ const queryCache = yield* LocalQueryCache;
34502
35143
  const files = yield* LocalApplicationFiles;
34503
35144
  const subscriptions = yield* Ref.make(HashMap.empty());
34504
35145
  const dependencyIndex = makeDependencyIndex();
@@ -34507,7 +35148,11 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34507
35148
  const writeSemaphore = yield* Semaphore.make(1);
34508
35149
  const write = yield* socket.writer;
34509
35150
  const send = Effect.fn("SyncServer.send")(function* (message) {
34510
- yield* writeSemaphore.withPermits(1)(Schema.encodeEffect(ServerMessageJson)(message).pipe(Effect.flatMap(write)));
35151
+ const bound = message.type === "Result" || message.type === "Preparation" || message.type === "Error" ? {
35152
+ ...message,
35153
+ sessionEpoch: identity.sessionEpoch
35154
+ } : message;
35155
+ yield* writeSemaphore.withPermits(1)(Schema.encodeEffect(ServerMessageJson)(bound).pipe(Effect.flatMap(write)));
34511
35156
  });
34512
35157
  const sendResolutionError = (operation, error, deliver = send) => {
34513
35158
  const code = error._tag;
@@ -34523,7 +35168,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34523
35168
  let prepared = previouslyPrepared;
34524
35169
  while (yield* isActive(subscriptionId, subscription)) {
34525
35170
  if (prepared === void 0) {
34526
- const resolved = yield* runtime.prepare(subscription.function, "Query", subscription.args).pipe(Effect.catchTags({
35171
+ const resolved = yield* runtime.prepare(subscription.function, "Query", subscription.args, identity).pipe(Effect.catchTags({
34527
35172
  FunctionUnavailable: (error) => sendResolutionError(operation, error, deliver),
34528
35173
  InvalidArguments: (error) => sendResolutionError(operation, error, deliver),
34529
35174
  UnknownFunction: (error) => sendResolutionError(operation, error, deliver),
@@ -34532,7 +35177,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34532
35177
  if (resolved === void 0) return;
34533
35178
  prepared = resolved;
34534
35179
  }
34535
- const result = yield* prepared.execute;
35180
+ const result = yield* queryCache.execute(subscription.queryKey, identity, yield* invalidation.latestRevision, prepared.execute);
34536
35181
  prepared = void 0;
34537
35182
  if (result.type !== "Query") return;
34538
35183
  if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
@@ -34543,7 +35188,12 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34543
35188
  type: "Snapshot",
34544
35189
  id: subscriptionId,
34545
35190
  result: result.result,
34546
- revision: result.observedRevision
35191
+ revision: result.observedRevision,
35192
+ identity: {
35193
+ sessionEpoch: identity.sessionEpoch,
35194
+ viewRevision: identity.viewRevision,
35195
+ dependsOnId: result.dependencies.some((dependency) => dependency.type === "Id")
35196
+ }
34547
35197
  };
34548
35198
  yield* deliver(granted.length === 0 ? snapshot : {
34549
35199
  ...snapshot,
@@ -34561,6 +35211,17 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34561
35211
  }))), FiberSet.run(invocationFibers), Effect.asVoid);
34562
35212
  };
34563
35213
  const refreshAll = Effect.fn("SyncServer.refreshAll")(function* () {
35214
+ if (refreshIdentity !== void 0) {
35215
+ const next = yield* refreshIdentity;
35216
+ const changed = idViewKey(identity) !== idViewKey(next) || identity.validUntil !== next.validUntil;
35217
+ identity = next;
35218
+ if (changed) yield* send({
35219
+ type: "Session",
35220
+ event: "Updated",
35221
+ session: identity,
35222
+ serverTime: yield* Clock.currentTimeMillis
35223
+ });
35224
+ }
34564
35225
  const current = yield* Ref.get(subscriptions);
34565
35226
  yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) => scheduleQuery(subscriptionId, subscription));
34566
35227
  });
@@ -34584,7 +35245,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34584
35245
  yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
34585
35246
  return;
34586
35247
  }
34587
- const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
35248
+ const prepared = yield* runtime.prepare(message.function, "Query", message.args, identity).pipe(Effect.catchTags({
34588
35249
  FunctionUnavailable: (error) => sendResolutionError(operation, error),
34589
35250
  InvalidArguments: (error) => sendResolutionError(operation, error),
34590
35251
  UnknownFunction: (error) => sendResolutionError(operation, error),
@@ -34609,7 +35270,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34609
35270
  yield* files.releasePreparation(message.id);
34610
35271
  return;
34611
35272
  }
34612
- const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
35273
+ const prepared = yield* runtime.prepare(message.function, "Mutation", message.args, identity).pipe(Effect.catchTags({
34613
35274
  FunctionUnavailable: (error) => sendResolutionError(operation, error),
34614
35275
  InvalidArguments: (error) => sendResolutionError(operation, error),
34615
35276
  UnknownFunction: (error) => sendResolutionError(operation, error),
@@ -34619,7 +35280,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34619
35280
  yield* files.releasePreparation(message.id);
34620
35281
  return;
34621
35282
  }
34622
- const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
35283
+ const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args, identity.sessionEpoch);
34623
35284
  yield* mutationReplay.execute(message.id, input, prepared.execute.pipe(Effect.flatMap((result) => result.type === "Mutation" ? Effect.succeed(result) : Effect.die("Expected mutation")))).pipe(Effect.flatMap((result) => {
34624
35285
  if (!("committedRevision" in result)) return send({
34625
35286
  type: "Result",
@@ -34676,7 +35337,9 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34676
35337
  });
34677
35338
  yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
34678
35339
  type: "Handshake",
34679
- ...localSyncIdentity
35340
+ ...localSyncIdentity,
35341
+ session: identity,
35342
+ serverTime: yield* Clock.currentTimeMillis
34680
35343
  }).pipe(Effect.orDie) }).pipe(Effect.ensuring(Ref.get(subscriptions).pipe(Effect.flatMap((active) => Effect.forEach(HashMap.keys(active), (subscriptionId) => files.releaseGrants(subscriptionId))), Effect.asVoid)));
34681
35344
  });
34682
35345
  var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
@@ -34686,8 +35349,9 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
34686
35349
  const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(IdGenerator.layer));
34687
35350
  const environmentLayer = DevelopmentEnvironment.layer(server, appDirectory).pipe(Layer.provide(layer$1), Layer.orDie);
34688
35351
  const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(Layer.merge(layer$1, environmentLayer))), executorLayer)));
34689
- const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, environmentLayer, queryInvalidationLayer, persistenceLayer, layer$1, localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(IdGenerator.layer, sqliteLayer, layer$1))));
35352
+ const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, environmentLayer, queryInvalidationLayer, persistenceLayer, layer$1, LocalId.layer(appDirectory).pipe(Layer.provide(layer$1)), localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(IdGenerator.layer, sqliteLayer, layer$1))));
34690
35353
  return Layer.effect(SyncHandlers, Effect.gen(function* () {
35354
+ const localId = yield* LocalId;
34691
35355
  const invalidation = yield* QueryInvalidation;
34692
35356
  const environment = yield* DevelopmentEnvironment;
34693
35357
  const fileSystem = yield* FileSystem.FileSystem;
@@ -34702,7 +35366,49 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
34702
35366
  }));
34703
35367
  const httpApp = Effect.gen(function* () {
34704
35368
  const request = yield* HttpServerRequest$1.HttpServerRequest;
34705
- const pathname = new URL(request.url, "http://ignotum.local").pathname;
35369
+ const url = new URL(request.url, `http://${request.headers.host ?? "ignotum.local"}`);
35370
+ const pathname = url.pathname;
35371
+ if (pathname === "/_ignotum/v1/session" && request.method === "GET") {
35372
+ const session = yield* localId.session(url.searchParams.get("username"), url.searchParams.get("epoch") ?? "dev:anonymous");
35373
+ return yield* HttpServerResponse.json({
35374
+ ...session,
35375
+ serverTime: yield* Clock.currentTimeMillis,
35376
+ expiresAt: session.validUntil,
35377
+ development: true
35378
+ }, { headers: { "cache-control": "no-store" } });
35379
+ }
35380
+ if (pathname === sessionRequestsPath && request.method === "POST") {
35381
+ if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
35382
+ const input = yield* request.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SessionRequest)), Effect.option);
35383
+ if (Option.isNone(input)) return HttpServerResponse.empty({ status: 400 });
35384
+ return yield* HttpServerResponse.json({
35385
+ redirectUrl: `${localIdPath}?request=${encodeURIComponent(JSON.stringify(input.value))}`,
35386
+ expiresAt: (yield* Clock.currentTimeMillis) + 6e5
35387
+ }, {
35388
+ status: 201,
35389
+ headers: { "cache-control": "no-store" }
35390
+ });
35391
+ }
35392
+ if (pathname === `/_ignotum/v1/id/profile` && request.method === "GET") {
35393
+ if (request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
35394
+ return yield* HttpServerResponse.json((yield* localId.profile(url.searchParams.get("username") ?? "")) ?? null, { headers: { "cache-control": "no-store" } });
35395
+ }
35396
+ if (pathname === "/_ignotum/v1/id" && request.method === "GET") {
35397
+ const input = yield* Schema.decodeEffect(Schema.fromJsonString(SessionRequest))(url.searchParams.get("request") ?? "").pipe(Effect.option);
35398
+ return Option.isNone(input) ? HttpServerResponse.empty({ status: 400 }) : HttpServerResponse.html(localIdPage(input.value));
35399
+ }
35400
+ if (pathname === "/_ignotum/v1/id" && request.method === "POST") {
35401
+ if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
35402
+ const input = yield* request.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(LocalCompletion)), Effect.option);
35403
+ if (Option.isNone(input)) return HttpServerResponse.empty({ status: 400 });
35404
+ yield* localId.complete(input.value);
35405
+ yield* invalidation.publish({ type: "All" });
35406
+ return HttpServerResponse.empty({ status: 204 });
35407
+ }
35408
+ if (pathname === "/_ignotum/v1/session" && request.method === "DELETE") {
35409
+ if (request.headers.origin !== url.origin || request.headers["x-ignotum-request"] !== "1") return HttpServerResponse.empty({ status: 403 });
35410
+ return HttpServerResponse.empty({ status: 204 });
35411
+ }
34706
35412
  if (pathname.startsWith(fileUploadUrlPrefix)) {
34707
35413
  if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
34708
35414
  const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
@@ -34736,8 +35442,13 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
34736
35442
  }, { status: pathname === syncPath ? 426 : 404 });
34737
35443
  });
34738
35444
  const socketApp = Effect.gen(function* () {
34739
- const socket = yield* (yield* HttpServerRequest$1.HttpServerRequest).upgrade;
34740
- yield* runSession(socket);
35445
+ const request = yield* HttpServerRequest$1.HttpServerRequest;
35446
+ const url = new URL(request.url, `http://${request.headers.host ?? "ignotum.local"}`);
35447
+ if (request.headers.origin !== url.origin) return HttpServerResponse.empty({ status: 403 });
35448
+ const refresh = localId.session(url.searchParams.get("username"), url.searchParams.get("epoch") ?? "dev:anonymous");
35449
+ const identity = yield* refresh;
35450
+ const socket = yield* request.upgrade;
35451
+ yield* runSession(socket, identity, refresh);
34741
35452
  return HttpServerResponse.empty();
34742
35453
  });
34743
35454
  const http = yield* makeHandler(httpApp, { scope });
@@ -34914,43 +35625,57 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
34914
35625
  }));
34915
35626
  });
34916
35627
  //#endregion
35628
+ //#region ../../docs/user/authentication.md?raw
35629
+ var authentication_default = "---\ntitle: Ignotum ID\ndescription: Sign users in and read their identity in queries and mutations.\nsection: Authentication\nslug: authentication\norder: 40\n---\n\n# Ignotum ID\n\nIgnotum ID lets people sign in to your app with Google. It is available in every app without an\nauthentication configuration file. The account people use in your app is separate from the\nIgnotum account you use to deploy it.\n\n## Add sign-in to your app\n\nUse `useId` from `ignotum/client`:\n\n```tsx\nimport { useId } from \"ignotum/client\";\n\nexport function Account() {\n const id = useId();\n\n if (id.status === \"pending\") return <p>Loading account...</p>;\n if (id.status === \"error\") return <p>{id.error}</p>;\n if (id.status === \"signedOut\") {\n return <button onClick={() => id.signIn()}>Sign in</button>;\n }\n\n return (\n <div>\n <p>{id.user.name ?? \"Signed in\"}</p>\n <button onClick={() => id.signOut()}>Sign out</button>\n </div>\n );\n}\n```\n\n`signIn()` opens Ignotum ID and returns to the current page when the person finishes or cancels.\nPass `returnTo: \"/account\"` to choose another page in your app. The destination must be a relative\npath within the app. The action's promise reports whether the redirect started. After returning,\n`id.outcome` reports `SignedIn`, `ProfileUpdated`, or `Cancelled` when available.\n\nSign-out ends this app's session. People can manage all their connected apps and sign out of their\nother sessions from the Ignotum ID account page.\n\n## Read the current user\n\nQueries and mutations receive `ctx.id`. Use `current()` when signed-out visitors are allowed:\n\n```ts\nconst user = yield * ctx.id.current();\nif (user === null) return [];\n```\n\nUse `require()` when the function needs a signed-in user:\n\n```ts\nconst user = yield * ctx.id.require();\n```\n\n`require()` returns the application error `IdRequired` when the visitor is signed out. Handle it\nwith `Result.match` as you would another application error.\n\nThe returned user has an `id` and optional `name`, `email`, and `image` fields. The same person has\na stable ID within your app, including after disconnecting and signing in again. Their ID differs\nbetween apps. Use the ID for ownership checks. Names and email addresses can change and should\nnot be used as ownership keys.\n\nIgnotum updates queries that read `ctx.id` when the user's shared profile changes. After sign-out\nor a loss of session verification, results that depend on identity return to pending until they\ncan be loaded for the current visitor.\n\n## Store ownership\n\nUse `values.userId()` for an Ignotum ID user reference:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n todos: table({\n ownerId: values.userId(),\n text: values.string(),\n }).index(\"by_owner\", [\"ownerId\"]),\n}));\n```\n\nSet the owner from `ctx.id.require()` in your mutation. Do not trust an owner ID supplied by Web\ncode. Check ownership before returning, updating, or deleting a private document. Signing in\nidentifies the caller; your functions decide which documents that caller may access.\n\nThe `User` and `UserId` types are exported from `ignotum/server` and `ignotum/client`.\n\nNext: [Profile sharing](profile-sharing.md)\n";
35630
+ //#endregion
35631
+ //#region ../../docs/user/profile-sharing.md?raw
35632
+ var profile_sharing_default = "---\ntitle: Profile sharing\ndescription: Request optional profile information and handle changes to sharing.\nsection: Authentication\nslug: profile-sharing\norder: 41\n---\n\n# Profile sharing\n\nSigning in gives your app a user ID. Profile information is optional. Request the fields your app\nneeds when starting sign-in:\n\n```tsx\nconst id = useId();\nawait id.signIn({ profile: [\"name\", \"image\"] });\n```\n\nRequested fields are selected by default on the Ignotum ID continue page. People can uncheck\nnew fields before continuing and change sharing later from their ID account page. A person can\nfinish sign-in without sharing any of them. Always handle absent fields in `id.user` and in the\nuser returned by `ctx.id`.\n\nA signed-in person can share more information later:\n\n```tsx\nawait id.requestProfile([\"email\"], { returnTo: \"/settings\" });\n```\n\nThis request keeps the current app account. Cancelling it leaves existing sharing in place.\nRequesting profile information while signed out fails; start with `signIn()` instead.\n\nPeople can change their name and image, withdraw sharing, or disconnect your app from the Ignotum\nID account page. Your active queries and `useId` update when those changes reach the app. Keep\nprofile fields optional in the UI and use `user.id` for stored references.\n\nA shared email comes from the person's verified Google account. A shared image is a URL you can\nuse in an image element. Treat that URL as temporary and read it from the current profile instead\nof storing it as a permanent address.\n\nNext: [Test authentication locally](local-authentication.md)\n";
35633
+ //#endregion
35634
+ //#region ../../docs/user/local-authentication.md?raw
35635
+ var local_authentication_default = "---\ntitle: Test locally\ndescription: Try signed-in and signed-out behavior with local usernames.\nsection: Authentication\nslug: local-authentication\norder: 42\n---\n\n# Test authentication locally\n\nRun `npx ignotum dev` and use your app's sign-in button. The local sign-in page lets you choose a\nusername such as `john`, enter a profile, and select which requested fields to share. You do not\nneed a Google account or provider credentials.\n\nProfile fields you leave blank or do not share are omitted from the user returned to your app.\nYou can sign in with just a username.\n\nLocal usernames are readable user IDs. Use lowercase letters, digits, underscores, or hyphens,\nstarting with a letter. Each browser tab keeps its own selected user, so you can test two people\nusing the app at once. Signing out affects that tab.\n\nUse `ctx.id.current()`, `ctx.id.require()`, and `useId()` in the same way locally and after\ndeployment. Test signed-out visitors, two different users, and a user who declines optional\nprofile sharing. Production user IDs are opaque; do not make application behavior depend on the\nshape of a local username.\n";
35636
+ //#endregion
34917
35637
  //#region ../../docs/agent/AGENTS.md?raw
34918
35638
  var AGENTS_default = "# Ignotum app instructions\n\nThis repository is an Ignotum app. Read `.agents/skills/ignotum/SKILL.md` before changing the\nschema, server functions, generated references, client code, app structure, or development setup.\n\n## Source boundaries\n\n- `server/schema.ts` defines tables and indexes.\n- TypeScript files directly inside `server` define public queries and mutations.\n- `client/index.tsx` default-exports the app definition and root component.\n- `shared` contains code that is safe in both client and server builds.\n- `_generated` is compiler output. Never edit it.\n\nUse `ignotum/server` only for schema authoring. Use generated builders from\n`@/_generated/server.js` in function modules. Use `ignotum/client` and\n`@/_generated/api.js` in client code. Keep the `.js` suffix on generated and aliased imports.\n\nDo not add an HTML entry, framework configuration, Tailwind configuration, API route, direct\ndatabase client, or backend server. Those do not belong to an Ignotum app.\n\n## Before finishing\n\nRun the app's existing checks. When no project-specific command says otherwise, regenerate the\nbindings and typecheck:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nDo not reset the development database unless the user explicitly asks to discard local data.\n";
34919
35639
  //#endregion
34920
35640
  //#region ../../docs/agent/skills/ignotum/SKILL.md?raw
34921
- var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Declare or use server environment variables | [Environment variables](references/environment-variables.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define server configuration only in `server/env.ts` with `defineEnv` and supply it from the\n project-root `.env.ignotum`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing a deployed app's fields, check\n[Deployment model](references/deployment-model.md). After the first\nactivation, hosted deployment requires the same table names, field names, and field validators.\nIndex-only changes are allowed. Do not promise that adding an optional field is compatible.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
35641
+ var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotum app, including its database, functions, files, Web code, generated references, local workflow, and deployment configuration.\n---\n\n# Work on an Ignotum app\n\nUse this skill for changes to an Ignotum app. The bundled references describe the installed\nIgnotum release. Prefer them to assumptions based on another backend or UI framework.\n\n## Inspect before editing\n\nRead the files that define the part of the app you are changing:\n\n- `server/schema.ts` for tables, fields, indexes, and generated document types;\n- TypeScript files directly inside `server` for queries and mutations;\n- `client/index.tsx` and imported client modules for the UI;\n- `shared` for code imported by both sides;\n- `package.json` and `tsconfig.json` for the available scripts and aliases.\n\nInspect `_generated` when you need the exact generated type or function address. Never edit it.\nGenerated files can lag behind source until `ignotum codegen` or the dev server runs.\n\nRead the smallest relevant set of references before changing code. If a task crosses several\nareas, read each corresponding page completely.\n\n## Choose the reference\n\n| Task | Reference |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| Create or understand an app | [Quickstart](references/quickstart.md) and [Project structure](references/project-structure.md) |\n| Change tables, fields, or IDs | [Schema](references/schema.md) and [Values](references/values.md) |\n| Define or query an index | [Indexes](references/indexes.md) |\n| Read or paginate data | [Reading data](references/reading-data.md) and [Pagination](references/pagination.md) |\n| Write data | [Writing data](references/writing-data.md) |\n| Add or call a query | [Functions](references/functions.md) and [Queries](references/queries.md) |\n| Add or call a mutation | [Functions](references/functions.md) and [Mutations](references/mutations.md) |\n| Define or handle application errors | [Errors](references/errors.md) |\n| Declare or use server environment variables | [Environment variables](references/environment-variables.md) |\n| Accept, store, return, or use application files | [Files](references/files.md) |\n| Change the app entry or Preact code | [Web](references/web.md), [App entry](references/app-entry.md), and [Preact](references/preact.md) |\n| Change styles, the icon, or public assets | [Styling](references/styling.md) and [Public assets](references/public-assets.md) |\n| Reason about transactions, ordering, retries, or live updates | [Guarantees](references/guarantees.md) |\n| Run, configure, or reset local development | [Local development](references/local-development.md) |\n| Update or inspect generated bindings | [Generated code](references/generated-code.md) |\n| Deploy or change the app link | [Deploy an app](references/deploy-app.md) and [Deployment model](references/deployment-model.md) |\n| Look up commands and flags | [CLI](references/cli.md) |\n| Check hosted capacity or argument sizes | [Limits](references/limits.md) |\n| Build deployment tooling against `api.ignotum.cloud` | [HTTP API](references/api.md) |\n\n## Preserve the app model\n\nAn Ignotum app has a fixed division of responsibility:\n\n- Define the schema only in `server/schema.ts` with `defineSchema` from `ignotum/server`.\n- Define server configuration only in `server/env.ts` with `defineEnv` and supply it from the\n project-root `.env.ignotum`.\n- Define functions in `.ts` files directly inside `server`. Nested files do not become API modules.\n- Import schema-bound `query`, `mutation`, `values`, and `Result` from\n `@/_generated/server.js`.\n- Import generated client references from `@/_generated/api.js`.\n- Import JSX, hooks, UI types, `app`, and query or mutation hooks from `ignotum/client`.\n- Put cross-boundary code in `shared` only when it uses APIs available to both the browser and\n server function environment.\n\nDo not add React, a separate Preact package, a router, an HTML entry, Vite configuration, Tailwind\nconfiguration, an ORM, a direct database connection, custom API routes, or a second backend unless\nthe user explicitly asks to leave the Ignotum application model. Authentication, actions,\nschedules, and workflows are not current Ignotum features.\n\nClient code cannot import server modules, server generated bindings, Node built-ins, or\n`ignotum/server`. Server code cannot import client modules, client generated references, Node\nbuilt-ins, or `ignotum/client`. Dynamic imports and native add-ons do not belong in server\nfunctions.\n\n## Change the schema carefully\n\nUse validators for every stored field. They provide runtime validation as well as TypeScript types.\nUse table-specific IDs for relationships and declare an index for ordered or filtered access that\nmust avoid a full table read.\n\nBefore changing stored fields, read [Migrations](references/migrations.md). Changes that preserve\nvalid documents work automatically. Use `.default()`, `.renameFrom()`, and `.from()` in\n`server/schema.ts` for missing values, renamed fields, and historical representations. Conversions\nmust be synchronous and deterministic. Use application mutations for changes that depend on other\ndocuments or external data, or change the meaning of values that already satisfy the validator.\n\nEvery stored document receives `id`, `createdAt`, and `updatedAt`. Do not declare those fields in\nthe schema or write them through insert, patch, or replace input.\n\n## Write functions through generated builders\n\nHandlers are generator functions. Use `yield*` for database operations and `return` for the public\nvalue. Declare `args`, `returns`, and application `errors` so the client receives accurate types and\nruntime checks.\n\nChoose the function kind by behavior:\n\n- A query reads one committed state and cannot write.\n- A mutation may read and write. Its writes commit together only after a valid success result.\n- Use a declared application error for an expected domain outcome. Do not throw it or convert it to\n a generic exception.\n\nUse `find` when a missing document is an ordinary branch. Use `get` when absence should fail with\n`DocumentNotFound`. `patch`, `replace`, and `delete` fail with the same error when their target is\nmissing. Check with `find` first when a missing write target should be ignored.\n\nIndexes order by their declared fields, then `createdAt`, then `id`. Add equality bounds for a\nleading prefix before using a range bound on the next field. Do not emulate an indexed filter by\ncollecting an unbounded table unless the table is intentionally small.\n\n## Use the client result model\n\n`useQuery` returns `Pending`, a value, or a declared error. Render each reachable state with\n`Result.match`. Pass `Query.skip` when the arguments are not ready instead of inventing placeholder\narguments.\n\n`useMutation` returns a stable typed function. Its promise resolves to the declared result and can\nreject for client or protocol failures. Internal function failures use the result's\n`InternalServerError` branch. A resolved success means its commit is complete, but subscribed\nqueries may update afterward.\n\nFor long ordered lists, use `usePaginatedQuery` with a function that calls `.paginate(...)`. Keep\ndocument IDs as rendering keys. Loaded pages remain live and do not form a frozen historical\nsnapshot.\n\nThe required `client/index.tsx` must default-export one direct `app({ title, component })` call.\nImport `app` by that name from `ignotum/client`, use a non-empty quoted title, and provide only the\n`title` and `component` properties. The optional icon path is exactly `client/icon.svg`. Put\nsupported public files in top-level `public`, not `client/public`.\n\n## Verify the change\n\nUse existing app scripts when they are stricter. Otherwise run:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nRun focused tests when the app has them. For behavior that depends on live results, also run the dev\nserver and exercise the affected query or mutation from the Web part.\n\nTreat `.ignotum/dev/state.db` as user data. Stop the dev server before `npx ignotum dev db reset`,\nand reset it only when the user asked for a clean local database. Treat `.ignotum/app.json` as the\nhosted app link; do not delete or rewrite it to solve a deployment problem without confirming that\nthe app should be relinked.\n";
34922
35642
  //#endregion
34923
35643
  //#region src/cli/agent-files.ts
34924
35644
  const references = [
35645
+ ["authentication.md", authentication_default],
35646
+ ["profile-sharing.md", profile_sharing_default],
35647
+ ["local-authentication.md", local_authentication_default],
34925
35648
  ["api.md", "---\ntitle: HTTP API\ndescription: Use the authenticated API at api.ignotum.cloud.\nsection: Reference\nslug: api\norder: 73\n---\n\n# HTTP API\n\nThe public API at `https://api.ignotum.cloud` creates apps and manages immutable deployments. The\n`ignotum deploy` command is the normal client for these endpoints. Use the HTTP API directly when\nbuilding deployment tooling or checking an app after an interrupted command.\n\nThe API does not expose application queries and mutations over HTTP. Deployed clients call those\nfunctions through the generated client API.\n\n## Authentication\n\nEvery route except `GET /health` requires a bearer token:\n\n```http\nAuthorization: Bearer <IGNOTUM_SESSION_TOKEN>\n```\n\n`ignotum auth login` obtains this revocable session through the device authorization flow. Treat it\nas an account credential. Do not include it in client code or send it from a deployed app. The API\ndoes not enable browser CORS, so call it from a CLI, CI job, or other trusted server environment.\n\nJSON requests require `Content-Type: application/json`. A JSON body may be at most 1 MiB. Responses\nuse JSON unless a route is not found.\n\n## Common values\n\nIgnotum IDs are opaque strings. Their prefixes identify the kind:\n\n```text\napp_000000000000000000000001\ndep_000000000000000000000001\nteam_000000000000000000000001\n```\n\nEach prefix is followed by 24 lowercase letters or digits. Do not parse or generate these IDs.\n\nTimestamps such as `createdAt` and `readyAt` are Unix time in milliseconds.\n\nAn app object has this shape:\n\n```json\n{\n \"id\": \"app_000000000000000000000001\",\n \"teamId\": \"team_000000000000000000000001\",\n \"slug\": \"team-todos\",\n \"activeDeploymentId\": \"dep_000000000000000000000001\",\n \"deploymentGeneration\": 3,\n \"createdAt\": 1787925600000\n}\n```\n\n`activeDeploymentId` is `null` before the first activation. `deploymentGeneration` starts at 0 and\nincreases when a deployment becomes active.\n\n## Health\n\n```http\nGET /health\n```\n\nA healthy API returns `200`:\n\n```json\n{ \"service\": \"api\", \"status\": \"ok\" }\n```\n\nAn infrastructure check failure returns `503` with `status` set to `unavailable`. This route does\nnot require authentication.\n\n## Create an app\n\n```http\nPOST /v1/apps\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"slug\": \"team-todos\" }\n```\n\nA slug is 1 through 63 lowercase letters, digits, or hyphens. It must start and end with a letter\nor digit. A successful request returns `201` and the new app object.\n\nThis operation is not idempotent. A repeated request for the same slug returns `SlugUnavailable`.\n\n## Get an app\n\nLook up an app by ID:\n\n```http\nGET /v1/apps/app_000000000000000000000001\nAuthorization: Bearer <token>\n```\n\nOr by slug:\n\n```http\nGET /v1/apps/by-slug/team-todos\nAuthorization: Bearer <token>\n```\n\nBoth routes return `200` and the same app shape. The authenticated account must own the app. The\nAPI currently has no route to list apps, rename a slug, or delete an app.\n\n## Deployment flow\n\nA deployment uses four steps:\n\n1. Begin with an inventory.\n2. Upload the canonical `inventory.json` and every file listed by it.\n3. Finalize the deployment.\n4. Activate the ready deployment.\n\nThe deployment is inactive until the last step succeeds.\n\n### Inventory\n\nThe begin request contains a version 1 inventory:\n\n```json\n{\n \"formatVersion\": 1,\n \"files\": [\n {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\n \"kind\": \"ClientShell\",\n \"contentType\": \"text/html; charset=utf-8\"\n }\n ]\n}\n```\n\nEach file entry has:\n\n| Field | Meaning |\n| ----------------- | ---------------------------------------------------------------------- |\n| `path` | Relative artifact path with `/` separators and no `.` or `..` segment. |\n| `size` | Byte length as a non-negative integer. |\n| `sha256` | Lowercase 64-character SHA-256 digest. |\n| `kind` | The artifact role listed below. |\n| `contentType` | MIME type stored with the file. |\n| `contentEncoding` | Optional encoding metadata. |\n\nThe defined kinds are `ClientAsset`, `ClientDocument`, `ClientManifest`, `ClientPublicFile`,\n`ClientShell`, `FunctionBundle`, `ServerManifest`, `SourceMap`, and `SchemaSnapshot`.\n\nFiles must be sorted by `path`. The inventory must contain `client/manifest.json`,\n`client/shell.html`, `server/manifest.json`, and `server/schema.json`. Paths and kinds must agree,\nand every manifest reference must match the inventory. `inventory.json` is not listed because it\ncannot contain its own hash.\n\nThe byte limits are in [Limits](limits.md). The public `ignotum` package does not expose an artifact\nbuilder or a build-only command. The formats below let deployment tooling inspect and transport an\nartifact emitted by the CLI. Building function bundles with another compiler is not a supported\npublic extension point.\n\nCanonical `inventory.json` is UTF-8 JSON with object keys sorted lexicographically, array order\npreserved, no insignificant whitespace, and one trailing line feed. Its upload headers contain the\nbyte length and SHA-256 digest of those exact bytes.\n\n### Client manifest\n\n`client/manifest.json` connects hosted URL paths to client files:\n\n```json\n{\n \"formatVersion\": 1,\n \"shell\": {\n \"path\": \"client/shell.html\",\n \"size\": 128,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"routes\": [\n {\n \"pathname\": \"/manual.pdf\",\n \"artifact\": {\n \"path\": \"client/routes/manual.pdf\",\n \"size\": 4096,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n }\n }\n ]\n}\n```\n\nThe shell reference must point to `client/shell.html`. Routes must be sorted by `pathname`. A\npathname starts with `/`, uses only letters, digits, `.`, `_`, `~`, `-`, and `/`, and cannot claim\n`/_ignotum`. Each route points to a matching `ClientDocument` or `ClientPublicFile` inventory entry.\nAn artifact can have only one route, and every document or public-file entry must have one.\n\nFiles under `client/assets/` use `ClientAsset`. Files under `client/routes/` use `ClientDocument`\nwhen their name ends in `.html`; AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files use\n`ClientPublicFile`. The four fixed client paths use their corresponding fixed kinds.\n\n### Server manifest\n\n`server/manifest.json` lists every callable function and the schema snapshot:\n\n```json\n{\n \"formatVersion\": 1,\n \"schema\": {\n \"path\": \"server/schema.json\",\n \"size\": 512,\n \"sha256\": \"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\"\n },\n \"functions\": [\n {\n \"address\": \"api.todos.list\",\n \"kind\": \"Query\",\n \"bundle\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs\",\n \"size\": 2048,\n \"sha256\": \"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789\"\n },\n \"sourceMap\": {\n \"path\": \"server/functions/todos/list-a1b2c3.mjs.map\",\n \"size\": 1024,\n \"sha256\": \"123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0\"\n }\n }\n ]\n}\n```\n\nFunction addresses have the form `api.<module>.<export>`, where both names are TypeScript\nidentifiers. `kind` is `Query` or `Mutation`. Addresses and referenced paths must be unique. A\nbundle ends in `.mjs`; its source map is the same path followed by `.map`. Every `server/` file is\nthe manifest, schema, or one referenced bundle or source map.\n\n### Schema snapshot\n\n`server/schema.json` is version 2:\n\n```json\n{\n \"formatVersion\": 2,\n \"tables\": [\n {\n \"name\": \"todos\",\n \"fields\": [\n { \"name\": \"completed\", \"value\": { \"type\": \"boolean\" } },\n { \"name\": \"text\", \"value\": { \"type\": \"string\" } }\n ],\n \"indexes\": [{ \"name\": \"by_completed\", \"fields\": [\"completed\"] }]\n }\n ]\n}\n```\n\nSort tables and fields by name. Sort indexes by name and preserve each index's declared field\norder. A value descriptor uses one of these recursive forms:\n\n```text\n{ \"type\": \"boolean\" | \"date\" | \"integer\" | \"never\" | \"null\" | \"number\" | \"string\" }\n{ \"type\": \"id\", \"table\": string }\n{ \"type\": \"literal\", \"value\": string | finite number | boolean }\n{ \"type\": \"literals\", \"values\": Array<string | finite number | boolean> }\n{ \"type\": \"array\" | \"nullable\" | \"optional\" | \"record\", \"value\": descriptor }\n{ \"type\": \"union\", \"members\": Array<descriptor> }\n{ \"type\": \"object\", \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n{ \"type\": \"error\", \"tag\": string, \"fields\": Array<{ \"name\": string, \"value\": descriptor }> }\n```\n\nSort object and error fields by name. This snapshot must describe the same schema used to build the\nfunction bundles. After the first activation, table and field descriptors must match the active\nsnapshot exactly; only the `indexes` arrays may change.\n\n### Begin\n\n```http\nPOST /v1/apps/{appId}/deployments\nContent-Type: application/json\nAuthorization: Bearer <token>\n\n{ \"inventory\": { \"formatVersion\": 1, \"files\": [] } }\n```\n\nThe abbreviated body above shows the envelope only. Send a complete valid inventory. Success\nreturns `201` and a deployment object:\n\n```json\n{\n \"id\": \"dep_000000000000000000000002\",\n \"appId\": \"app_000000000000000000000001\",\n \"status\": \"Uploading\",\n \"inventory\": { \"formatVersion\": 1, \"files\": [] },\n \"createdAt\": 1787925601000,\n \"readyAt\": null\n}\n```\n\nThe response repeats the complete accepted inventory. This operation is not idempotent. Repeating\nit creates another deployment.\n\n### Upload a file\n\nUpload each path as one percent-encoded route component. For example,\n`client/shell.html` becomes `client%2Fshell.html`:\n\n```http\nPUT /v1/apps/{appId}/deployments/{deploymentId}/files/client%2Fshell.html\nAuthorization: Bearer <token>\nContent-Length: 128\nX-Ignotum-Sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n<128 raw bytes>\n```\n\n`Content-Length` and `X-Ignotum-Sha256` must match the accepted inventory entry. The body is the raw\nfile, not JSON. Upload the canonical `inventory.json` through the same route even though it is not\nin the `files` array.\n\nA new file returns:\n\n```json\n{ \"status\": \"Stored\" }\n```\n\nUploading the same path, bytes, and metadata again is safe and returns `AlreadyStored`. Different\ncontent at an existing path returns `DeploymentFileConflict`; deployment files are immutable.\n\n### Finalize\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/finalize\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Finalization checks that every expected file exists, no extra file exists,\nall metadata matches, and the client and server manifests are valid. Success returns `200` with the\ndeployment status changed to `Ready` and `readyAt` set.\n\nFinalizing an already ready deployment is safe and returns its current record.\n\n### Activate\n\n```http\nPOST /v1/apps/{appId}/deployments/{deploymentId}/activate\nAuthorization: Bearer <token>\n```\n\nThe request has no body. Only a ready, unexpired deployment can be activated. Ignotum rejects a\nstored-schema change but permits index changes. See\n[Deployment model](deployment-model.md#schema-compatibility).\n\nSuccess returns `200` with the updated app object. Activating its current deployment again is safe.\nActivation reloads connected app clients.\n\n## Errors\n\nEvery recognized API failure has this body:\n\n```json\n{\n \"code\": \"SlugUnavailable\",\n \"message\": \"The app slug is unavailable.\"\n}\n```\n\nTreat `code` as the machine-readable value. Messages may change.\n\n| Status | Codes |\n| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `400` | `InvalidRequest`, `DeploymentFileUnexpected` |\n| `401` | `Unauthorized` |\n| `403` | `AppAccessDenied` |\n| `404` | `AppNotFound`, `DeploymentNotFound` |\n| `409` | `SlugUnavailable`, `DeploymentNotUploading`, `DeploymentFileConflict`, `DeploymentIncomplete`, `DeploymentNotReady`, `SchemaIncompatible`, `ActivationConflict` |\n| `503` | `ApiUnavailable` |\n\n`ApiUnavailable` does not prove whether a state-changing request reached its final step. Read the\napp before deciding how to recover from an unconfirmed activation. Do not automatically retry\n`POST /v1/apps` or the begin-deployment request because either may create a second resource.\n"],
34926
35649
  ["app-entry.md", "---\ntitle: App entry\ndescription: Define the Web title and root component in client/index.tsx.\nsection: Web\nslug: app-entry\norder: 41\n---\n\n# App entry\n\nThe required `client/index.tsx` file names the page and supplies its root component:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nKeep the entry in this form:\n\n- Import `app` by name from `ignotum/client`, without an alias.\n- Default-export one direct `app(...)` call.\n- Pass one object with exactly `title` and `component`.\n- Write both keys as unquoted properties, without a spread.\n- Use a non-empty quoted string literal for `title`.\n\nThe component can import and render other client modules.\n\nIgnotum does not use an `App.tsx` entry convention. The entry is `client/index.tsx`, and its default\nexport is the `app(...)` definition.\n\nNext: [Preact](preact.md)\n"],
34927
- ["cli.md", "---\ntitle: CLI\ndescription: Look up Ignotum commands, flags, and environment variables.\nsection: Reference\nslug: cli\norder: 72\n---\n\n# CLI\n\nRun commands through the local or downloaded `ignotum` package:\n\n```sh\nnpx ignotum <command>\n```\n\n## Commands\n\n| Command | Purpose |\n| ---------------------- | ------------------------------------------------------------------ |\n| `ignotum new <path>` | Create an app in an empty directory. |\n| `ignotum install` | Install dependencies with pnpm, or npm if pnpm is absent. |\n| `ignotum codegen` | Update files under `_generated`. |\n| `ignotum dev` | Run the local development server. |\n| `ignotum dev db reset` | Delete the local development database while the server is stopped. |\n| `ignotum auth login` | Sign in with GitHub through the device authorization flow. |\n| `ignotum auth logout` | Revoke the session and remove it from this computer. |\n| `ignotum auth status` | Show the signed-in account and team. |\n| `ignotum deploy` | Build, upload, and activate the app. |\n\nUse `ignotum <command> --help` for the command's generated help.\n\n## `new`\n\n```sh\nnpx ignotum new my-app\nnpx ignotum new . --no-git --no-install\n```\n\nThe target directory must be empty. The command installs dependencies and initializes a Git\nrepository by default. Use `--no-install` or `--no-git` to skip either action.\n\n## `dev`\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------- |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. |\n| `--open` | off | Open the app after the server starts. |\n\n## `deploy`\n\nUse `--app <slug>` to create or link the hosted app on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nLater deployments read the link from `.ignotum/app.json` and do not need the flag.\n\n## `auth`\n\nRun `npx ignotum auth login` once before deploying. The terminal prints a short code and opens the\napproval page. Confirm that the browser and terminal show the same code before approving it.\n`auth status` checks the saved session, and `auth logout` revokes it.\n\n## Environment variables\n\n| Variable | Meaning |\n| ------------------ | -------------------------------------------------------- |\n| `IGNOTUM_AUTH_URL` | Auth base URL. Defaults to `https://auth.ignotum.cloud`. |\n| `IGNOTUM_API_URL` | API base URL. Defaults to `https://api.ignotum.cloud`. |\n\n`IGNOTUM_API_URL` must use HTTPS. A numeric loopback address such as `127.0.0.1` or `[::1]` may\nuse HTTP for local platform development.\n"],
34928
- ["database.md", "---\ntitle: Database overview\nlabel: Overview\ndescription: Understand the persistent data model in an Ignotum app.\nsection: Database\nslug: database\norder: 10\n---\n\n# Database overview\n\nEach Ignotum app has a database described by `server/schema.ts`. A schema contains tables, stored\nfields, and indexes. Every stored document also has a table-specific ID and creation and update\ntimestamps.\n\nQueries and mutations read through `ctx.db`. Only mutations can insert, patch, replace, or delete\ndocuments. All reads and writes in one mutation commit together.\n\n- [Schema](schema.md) defines tables, fields, IDs, and schema changes.\n- [Values](values.md) lists the validators used for stored fields and function contracts.\n- [Reading data](reading-data.md) covers document and table reads.\n- [Writing data](writing-data.md) covers database changes and transaction behavior.\n- [Indexes](indexes.md) covers index definitions, ranges, and ordering.\n- [Pagination](pagination.md) covers cursor-based database and Web pagination.\n\nNext: [Schema](schema.md)\n"],
34929
- ["deploy-app.md", "---\ntitle: Deploy an app\ndescription: Link an app and publish a deployment to Ignotum Cloud.\nsection: Deployments\nslug: deploy\norder: 51\n---\n\n# Deploy an app\n\nSign in through the browser device flow:\n\n```sh\nnpx ignotum auth login\n```\n\nThe command opens `auth.ignotum.cloud`, asks you to sign in with GitHub, and requires you to approve\nthe code shown in the terminal. The CLI stores the resulting session under your user config\ndirectory with owner-only file permissions. It never writes the session into the app directory.\nIgnotum Cloud accounts are currently invite-only; the email returned by GitHub must have a pending\nplatform invite before the first sign-in.\n\n## First deployment\n\nChoose the app slug on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nSlugs use lowercase letters, numbers, and hyphens. They are 1 through 63 characters, cannot start\nor end with a hyphen, and must be globally available. The deployed app uses\n`https://my-app.ignotum.app`.\n\nIf the slug already belongs to your account, the CLI links the local directory to that app. If it\nis available, the CLI creates the app. If you omit `--app`, it asks for a slug in an interactive\nterminal.\n\nThe CLI writes the app ID, slug, and API URL to `.ignotum/app.json`. The generated `.gitignore`\nexcludes `.ignotum`.\n\n## Later deployments\n\nOnce linked, run:\n\n```sh\nnpx ignotum deploy\n```\n\nThe command updates generated code, builds and validates the complete app, uploads a new\ndeployment, and activates it. There is no separate public build command.\n\nThe active deployment stays unchanged if any of these steps fail. See\n[Deployment model](deployment-model.md) for activation, schema compatibility, and interrupted\ndeployments.\n\nNext: [Deployment model](deployment-model.md)\n"],
34930
- ["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\nThe deployment also captures `.env.ignotum`; later edits do not change an existing deployment.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command. An older deployment retains its original environment if it is\nreactivated through the HTTP API. Deploying older source again creates a new deployment with the\ncurrent `.env.ignotum` values.\n\n## Schema compatibility\n\nThe first active deployment fixes the stored table and field schema. Later deployments may change\nWeb code, functions, and indexes. Changing a table name, adding or removing a stored field, or\nchanging a field validator causes activation to fail with `SchemaIncompatible`. The active\ndeployment and stored data remain unchanged.\n\nAdding, removing, renaming, or reordering indexes is supported. Ignotum prepares new indexes before\nthe new deployment starts accepting mutations.\n\n## Failed and interrupted deployments\n\nA build, upload, validation, compatibility, or activation failure leaves the current deployment\nactive.\n\nIf an upload or finalization request loses its connection, the CLI prints the new deployment ID.\nUnfinished uploads expire after 24 hours. If activation cannot be confirmed, use the\n[HTTP API](api.md) to inspect the app's `activeDeploymentId`, or run `ignotum deploy` again to create\nand activate a new deployment.\n\nSee [Guarantees](guarantees.md) for the activation contract and [Limits](limits.md) for deployment\nretention.\n"],
34931
- ["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema,\nserver environment snapshot, and public assets. Ignotum activates these parts together after the\ncomplete deployment passes its checks.\n\nUse `ignotum deploy` to link a local project to a hosted app and publish it. A failed build, upload,\nvalidation, or activation does not replace the active deployment.\n\n- [Deploy an app](deploy-app.md) covers the command, first-time linking, and later deployments.\n- [Deployment model](deployment-model.md) covers activation, schema compatibility, and interrupted\n deployments.\n- [Limits](limits.md) lists deployment size and retention limits.\n\nNext: [Deploy an app](deploy-app.md)\n"],
35650
+ ["cli.md", "---\ntitle: CLI\ndescription: Look up Ignotum commands, flags, and environment variables.\nsection: Reference\nslug: cli\norder: 72\n---\n\n# CLI\n\nRun commands through the local or downloaded `ignotum` package:\n\n```sh\nnpx ignotum <command>\n```\n\n## Commands\n\n| Command | Purpose |\n| ---------------------- | ------------------------------------------------------------------ |\n| `ignotum new <path>` | Create an app in an empty directory. |\n| `ignotum install` | Install dependencies with pnpm, or npm if pnpm is absent. |\n| `ignotum codegen` | Update files under `_generated`. |\n| `ignotum dev` | Run the local development server. |\n| `ignotum dev db reset` | Delete the local development database while the server is stopped. |\n| `ignotum auth login` | Sign in with GitHub through the device authorization flow. |\n| `ignotum auth logout` | Revoke the session and remove it from this computer. |\n| `ignotum auth status` | Show the signed-in account and team. |\n| `ignotum deploy` | Build, upload, and activate the app. |\n\nUse `ignotum <command> --help` for the command's generated help.\n\n## `new`\n\n```sh\nnpx ignotum new my-app\nnpx ignotum new . --no-git --no-install\n```\n\nThe target directory must be empty. The command installs dependencies and initializes a Git\nrepository by default. Use `--no-install` or `--no-git` to skip either action.\n\n## `dev`\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------- |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. |\n| `--open` | off | Open the app after the server starts. |\n\n## `deploy`\n\nUse `--app <slug>` to create or link the hosted app on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nLater deployments read the link from `.ignotum/app.json` and do not need the flag.\n\nUse `ignotum deploy --resume <deployment-id>` to finish an interrupted activation using the\nalready uploaded deployment. This skips the build and upload. See [Migrations](migrations.md)\nfor schema changes and recovery.\n\n## `auth`\n\nRun `npx ignotum auth login` once before deploying. The terminal prints a short code and opens the\napproval page. Confirm that the browser and terminal show the same code before approving it.\n`auth status` checks the saved session, and `auth logout` revokes it.\n\n## Environment variables\n\n| Variable | Meaning |\n| ------------------ | -------------------------------------------------------- |\n| `IGNOTUM_AUTH_URL` | Auth base URL. Defaults to `https://auth.ignotum.cloud`. |\n| `IGNOTUM_API_URL` | API base URL. Defaults to `https://api.ignotum.cloud`. |\n\n`IGNOTUM_API_URL` must use HTTPS. A numeric loopback address such as `127.0.0.1` or `[::1]` may\nuse HTTP for local platform development.\n"],
35651
+ ["database.md", "---\ntitle: Database overview\nlabel: Overview\ndescription: Understand the persistent data model in an Ignotum app.\nsection: Database\nslug: database\norder: 10\n---\n\n# Database overview\n\nEach Ignotum app has a database described by `server/schema.ts`. A schema contains tables, stored\nfields, and indexes. Every stored document also has a table-specific ID and creation and update\ntimestamps.\n\nQueries and mutations read through `ctx.db`. Only mutations can insert, patch, replace, or delete\ndocuments. All reads and writes in one mutation commit together.\n\n- [Schema](schema.md) defines tables, fields, and IDs.\n- [Migrations](migrations.md) covers changes to schemas and stored data.\n- [Values](values.md) lists the validators used for stored fields and function contracts.\n- [Reading data](reading-data.md) covers document and table reads.\n- [Writing data](writing-data.md) covers database changes and transaction behavior.\n- [Indexes](indexes.md) covers index definitions, ranges, and ordering.\n- [Pagination](pagination.md) covers cursor-based database and Web pagination.\n\nNext: [Schema](schema.md)\n"],
35652
+ ["deploy-app.md", "---\ntitle: Deploy an app\ndescription: Link an app and publish a deployment to Ignotum Cloud.\nsection: Deployments\nslug: deploy\norder: 51\n---\n\n# Deploy an app\n\nSign in through the browser device flow:\n\n```sh\nnpx ignotum auth login\n```\n\nThe command opens `auth.ignotum.cloud`, asks you to sign in with GitHub, and requires you to approve\nthe code shown in the terminal. The CLI stores the resulting session under your user config\ndirectory with owner-only file permissions. It never writes the session into the app directory.\nIgnotum Cloud accounts are currently invite-only; the email returned by GitHub must have a pending\nplatform invite before the first sign-in.\n\n## First deployment\n\nChoose the app slug on the first deployment:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nSlugs use lowercase letters, numbers, and hyphens. They are 1 through 63 characters, cannot start\nor end with a hyphen, and must be globally available. The deployed app uses\n`https://my-app.ignotum.app`.\n\nIf the slug already belongs to your account, the CLI links the local directory to that app. If it\nis available, the CLI creates the app. If you omit `--app`, it asks for a slug in an interactive\nterminal.\n\nThe CLI writes the app ID, slug, and API URL to `.ignotum/app.json`. The generated `.gitignore`\nexcludes `.ignotum`.\n\n## Later deployments\n\nOnce linked, run:\n\n```sh\nnpx ignotum deploy\n```\n\nThe command updates generated code, builds and validates the complete app, uploads a new\ndeployment, and activates it. There is no separate public build command.\n\nBuild, upload, and schema validation failures leave the current deployment active. If activation\ncannot be confirmed, use `npx ignotum deploy --resume <deployment-id>` to finish it. See\n[Deployment model](deployment-model.md) for activation, schema compatibility, and interrupted\ndeployments.\n\nNext: [Deployment model](deployment-model.md)\n"],
35653
+ ["console.md", "---\ntitle: Console\nlabel: Console\ndescription: Browse live data, connected IDs, and deployments for your apps.\nsection: Deployments\nslug: console\norder: 54\n---\n\n# Console\n\nOpen [Ignotum Console](https://console.ignotum.cloud) and sign in with the GitHub account you use\nfor Ignotum. Select an app to open its Data, Deployments, and ID tabs.\n\n## Data\n\nChoose a table in the sidebar to browse its documents. Empty tables appear too. Columns show\n`id` first, followed by your top-level fields in alphabetical order, then `updatedAt` and\n`createdAt`. Dates use your browser's local time.\n\nExpand objects and arrays inside a cell to inspect their contents. Hover over or focus a document\nreference to preview the referenced document. An ID reference shows the profile that person\ncurrently shares with this app. Select a file thumbnail or filename to open a larger image preview.\n\nData updates as your app changes. You can browse pages of documents, but you cannot edit or delete\nthem in Console.\n\n## Deployments\n\nDeployments appear newest first. The current deployment has a **Current** badge.\n\n- **Activate** publishes a ready deployment that has never been active.\n- **Rollback** returns to a ready deployment that was active before.\n\nConfirm the change to make the selected deployment live. The same compatibility checks apply as\nwhen you deploy from the command line. A rollback keeps your app's current data; it does not\nrestore an earlier copy of the database. See [Deployment model](deployment-model.md).\n\nOlder deployments disappear when they are no longer retained. See [Limits](limits.md).\n\n## ID\n\nThe ID tab lists people currently connected to this app. It shows their app ID, shared image,\nname, email, and connection date. Fields they have not shared stay blank.\n\nConnections remain visible when someone signs out. If they disconnect the app, their row\ndisappears. Removing a shared profile field clears it from the table and any open profile preview.\nThese changes update live. You cannot change profiles or connections in Console.\n\nIf the connection drops, live views clear while Console reconnects. Sign in again if your platform\nsession has expired.\n"],
35654
+ ["deployment-model.md", "---\ntitle: Deployment model\ndescription: Understand activation, compatibility, and failure behavior.\nsection: Deployments\nslug: deployment-model\norder: 52\n---\n\n# Deployment model\n\nIgnotum creates a new immutable deployment for each successful `ignotum deploy` run. The active\ndeployment does not change until the new deployment has uploaded, passed validation, and activated.\nThe deployment also captures `.env.ignotum`; later edits do not change an existing deployment.\n\n## Activation\n\nActivation switches the Web part and functions together. Connected browser sessions reload after\nthey learn that the deployment changed, so a session does not intentionally combine Web code from\none deployment with functions from another.\n\nThe CLI has no rollback command. An older deployment retains its original environment if it is\nreactivated through the HTTP API. Deploying older source again creates a new deployment with the\ncurrent `.env.ignotum` values.\n\n## Schema compatibility\n\nSchema changes are validated against existing documents before activation. Changes that preserve\nvalid data work automatically; defaults, renames, and conversions handle changes to stored values.\nSee [Migrations](migrations.md).\n\nApp operations pause during schema preparation and activation. Data and indexes are ready before\nthe new deployment starts serving requests. A validation or conversion failure leaves stored data\nunchanged and resumes the current deployment.\n\n## Failed and interrupted deployments\n\nA build, upload, validation, or conversion failure leaves the current deployment active.\n\nIf an upload or finalization request loses its connection, the CLI prints the new deployment ID.\nUnfinished uploads expire after 24 hours. If activation cannot be confirmed, the app may remain\npaused. Finish the same activation with `ignotum deploy --resume <deployment-id>`. This uses the\nuploaded code and environment without rebuilding. You can inspect the app's `activeDeploymentId`\nthrough the [HTTP API](api.md).\n\nSee [Guarantees](guarantees.md) for the activation contract and [Limits](limits.md) for deployment\nretention.\n"],
35655
+ ["deployments.md", "---\ntitle: Deployments overview\nlabel: Overview\ndescription: Understand what an Ignotum deployment publishes.\nsection: Deployments\nslug: deployments\norder: 50\n---\n\n# Deployments overview\n\nA deployment publishes the complete Ignotum app. It contains the Web part, functions, schema,\nserver environment snapshot, and public assets. Ignotum activates these parts together after the\ncomplete deployment passes its checks.\n\nUse `ignotum deploy` to link a local project to a hosted app and publish it. A failed build, upload,\nvalidation, or activation does not replace the active deployment.\n\n- [Deploy an app](deploy-app.md) covers the command, first-time linking, and later deployments.\n- [Deployment model](deployment-model.md) covers activation, schema compatibility, and interrupted\n deployments.\n- [Console](console.md) lets you browse live data, connected IDs, and deployment history.\n- [Limits](limits.md) lists deployment size and retention limits.\n\nNext: [Deploy an app](deploy-app.md)\n"],
34932
35656
  ["errors.md", "---\ntitle: Errors\ndescription: Define, return, and handle typed application errors.\nsection: Functions\nslug: errors\norder: 24\n---\n\n# Errors\n\nFunctions can return typed application errors to the client. Ignotum keeps runtime failures and\nthrown exceptions separate from those public errors.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nIn a mutation, an application error rolls back every write from that invocation. In a query, it\nbecomes the settled failure for the subscription at the revision the query observed.\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n\nAn internal failure also rolls back a mutation. It does not become one of the application's\ndeclared errors, even if a thrown value happens to have the same shape.\n\n## Web matching\n\nThe client `Result.match` separates `error` and `internalError`. `error` handles only errors declared\nby the function. `internalError` handles the reserved platform error. If `internalError` is absent,\nmatching that result throws the error to the nearest UI error boundary.\n\nProtocol and connection failures are outside the application `Result`. A mutation promise rejects\nfor an operation-scoped protocol failure. A query hook throws a subscription infrastructure\nfailure during rendering so the UI error boundary can handle it.\n"],
34933
35657
  ["environment-variables.md", "---\ntitle: Environment variables\ndescription: Declare, validate, and read server-only configuration.\nsection: Functions\nslug: environment-variables\norder: 24\n---\n\n# Environment variables\n\nIgnotum provides typed, server-only configuration through `ctx.env`. Declare the variables in\n`server/env.ts` and supply their values in `.env.ignotum` at the project root. The same file is used\nfor local development and deployment.\n\n```ts\nimport { defineEnv } from \"ignotum/server\";\n\nexport default defineEnv(({ values }) => ({\n API_BASE_URL: values.string(),\n API_TOKEN: values.secret(),\n RETRY_LIMIT: values.integer(),\n SERVICE_ACCOUNT: values.secret(\n values.object({\n clientEmail: values.string(),\n privateKey: values.string(),\n }),\n ),\n}));\n```\n\nNames must contain uppercase letters, digits, and underscores, cannot begin with a digit, and cannot\nuse the reserved `IGNOTUM_` prefix.\n\nAdd the matching values to `.env.ignotum`:\n\n```dotenv\nAPI_BASE_URL=https://api.example.com\nAPI_TOKEN=replace-me\nRETRY_LIMIT=3\nSERVICE_ACCOUNT={\"clientEmail\":\"service@example.com\",\"privateKey\":\"replace-me\"}\n```\n\nEach value is tried as a string first. If that does not match its validator, Ignotum parses it as\nJSON and tries again. Numbers, booleans, arrays, records, and objects therefore use JSON syntax. If\na union accepts a string, the original string wins. Empty strings are preserved, and values are not\nread from the process environment.\n\nThe declaration and values must agree exactly. A missing file, missing required variable, extra\nvariable, or invalid value stops development startup or deployment. A `.env.ignotum` file without\n`server/env.ts` is also an error. When neither file exists, `ctx.env` is an empty object.\n\n## Read values in a function\n\nPlain values are available directly. Secret values require an explicit unwrap:\n\n```ts\nimport { Secret } from \"ignotum/server\";\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const status = query({\n returns: values.string(),\n handler: function* (ctx) {\n const token = Secret.value(ctx.env.API_TOKEN);\n return `${ctx.env.API_BASE_URL}:${token.length}`;\n },\n});\n```\n\n`values.secret()` is shorthand for `values.secret(values.string())`. A secret can wrap another\nenvironment validator, including an object. Until it is unwrapped, string conversion, inspection,\nand JSON serialization display `<redacted>` rather than its value. Once application code calls\n`Secret.value`, it is responsible for the plain value and can disclose or store it.\n\nSecret validators are available only inside `defineEnv`. They cannot be used for database fields,\nfunction arguments, results, or application errors.\n\n## Development and deployment\n\nChanging `.env.ignotum` during development reloads the validated environment and refreshes active\nqueries. An invalid edit makes functions unavailable until the file is fixed; Ignotum does not keep\nserving the previous values.\n\nEvery deployment captures an immutable snapshot of `.env.ignotum`. Changing the file requires a\nnew deployment. Reactivating an older deployment restores the environment captured with that\ndeployment.\n\nEnvironment values stay out of browser output and local build files. Hosted values are kept in\nprivate storage encrypted by the cloud provider and are supplied only to the isolated function\nruntime. This Private Alpha release does not provide customer-managed encryption keys or\noperator-blind encryption. Authorized Ignotum platform operators and the cloud storage provider\nremain within the trust boundary.\n\nKeep `.env.ignotum` out of Git. New Ignotum projects ignore `.env.*` and retain the `!.env.example`\nexception, so a value-free `.env.example` can document the required names. Existing projects should\nadd an equivalent ignore rule.\n\nSee [Functions](functions.md), [Values](values.md), and [Limits](limits.md).\n"],
34934
35658
  ["files.md", "---\ntitle: Files\ndescription: Validate, store, return, and use application files.\nsection: Files\nslug: files\norder: 30\n---\n\n# Files\n\nApplication files are database values. A Web form can pass a browser `File` to a mutation. The\nmutation stores its validated file value in a document, and a query can return it to Web code.\n\n## Declare a file value\n\nUse `values.file` in `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n profiles: table({\n name: values.string(),\n avatar: values.optional(values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 })),\n }),\n}));\n```\n\n`formats` must be a non-empty subset of `jpeg`, `png`, `webp`, `avif`, and `gif`. `maxBytes` is\noptional and cannot exceed the 10 MiB per-file limit. PDF, audio, video, SVG, archives, code, and\nexecutables are not accepted as application files.\n\n## Accept and store a file\n\nA mutation can accept the same file validator and store the result:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const setAvatar = mutation({\n args: {\n profileId: values.id(\"profiles\"),\n avatar: values.file({ formats: [\"jpeg\", \"png\", \"webp\"], maxBytes: 2_000_000 }),\n },\n\n handler: function* (ctx, args) {\n yield* ctx.db.patch(\"profiles\", args.profileId, { avatar: args.avatar });\n },\n});\n```\n\nThe handler receives `name`, `format`, `mimeType`, and `size`, not the file bytes. Hosted functions\ncannot open or inspect file contents. Ignotum validates the file before invoking the mutation.\n\nMutation returns and application errors cannot contain files. Query arguments cannot contain\nfiles.\n\nIn Web code, pass a native `File` to the generated mutation caller:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction AvatarForm({ profileId }: { profileId: Id<\"profiles\"> }) {\n const setAvatar = useMutation(api.profiles.setAvatar);\n\n const change = (event: Event) => {\n const input = event.currentTarget as HTMLInputElement;\n const avatar = input.files?.[0];\n if (avatar === undefined) return;\n\n void setAvatar({ profileId, avatar }).then(\n Result.match({\n value: () => console.log(\"Saved\"),\n error: () => console.log(\"Could not save\"),\n }),\n );\n };\n\n return <input type=\"file\" accept=\"image/jpeg,image/png,image/webp\" onChange={change} />;\n}\n```\n\nThe browser's `File.type` must identify an allowed format. Ignotum also verifies the file bytes.\n\n## Return and use a file\n\nQueries can return file fields, including nested files and complete documents:\n\n```ts\nexport const get = query({\n args: { id: values.id(\"profiles\") },\n returns: values.doc(\"profiles\"),\n\n handler: function* (ctx, args) {\n return yield* ctx.db.get(\"profiles\", args.id);\n },\n});\n```\n\nUse `Files.url` on the current query value:\n\n```tsx\nimport { Files, Result, useQuery } from \"ignotum/client\";\n\nconst profile = useQuery(api.profiles.get, { id: profileId });\n\nreturn Result.match(profile, {\n pending: () => <span>Loading...</span>,\n value: (value) =>\n value.avatar === undefined ? (\n <span>No avatar</span>\n ) : (\n <img src={Files.url(value.avatar)} alt=\"\" />\n ),\n error: () => <span>Unavailable</span>,\n});\n```\n\nThe returned same-origin URL works in images, links, CSS URLs, and `fetch`. It is not a permanent\nURL. It remains valid while the query subscription and its current result authorize that file.\nUnsubscribing, disconnecting, replacing the result, making the query stale, or deleting the final\ndatabase reference revokes access. Do not save the URL as application data.\n\nCalling `Files.url` on a native file or a value that did not come from an active query throws.\n\n## Copy and remove references\n\nA file value returned by a query can be passed to another compatible mutation argument:\n\n```tsx\nconst copyAvatar = useMutation(api.profiles.copyAvatar);\nvoid copyAvatar({ targetId, avatar: source.avatar });\n```\n\nThe destination validator must allow the file's format and size. Removing one field or document\nremoves that reference. Deleting the final database reference deletes the application file. A\nstale Web value cannot restore it afterward.\n\nDocument changes and file references commit together. A failed mutation does not leave references\npartially changed.\n\n## Limits\n\n- One file is limited to 10 MiB.\n- One mutation may add up to 8 new files and 25 MiB of file data.\n- One app may have up to 100 MiB of uncommitted file data.\n- One query result may contain up to 256 distinct files.\n\nApplication files differ from files in `public`. Public assets belong to a deployment and have\nstable paths. See [Public assets](public-assets.md).\n\nNext: [Deployments](deployments.md)\n"],
34935
35659
  ["functions.md", "---\ntitle: Functions overview\nlabel: Overview\ndescription: Define callable application logic and use its generated API.\nsection: Functions\nslug: functions\norder: 21\n---\n\n# Functions overview\n\nQueries and mutations are callable application functions. Define them in `.ts` files directly\ninside `server`. The file name becomes the generated API module, and each exported function keeps\nits export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nFunction module names must be valid TypeScript identifiers. Ignotum ignores `schema.ts`, `env.ts`,\n`index.ts`, files ending in `.test.ts` or `.spec.ts`, and names beginning with `_`. Nested files do\nnot become API modules.\n\n## Definitions\n\nImport schema-bound builders from the generated server file:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix. The generated definitions provide types and runtime validation for\narguments, successful returns, and application errors.\n\nThe `args` object describes one object parameter. Omit it when the function takes no arguments.\nOmit `returns` only when the handler returns `void`. Ignotum does not infer a runtime validator\nfrom a TypeScript return type.\n\nHandlers are generator functions. Use `yield*` for database and `Result` operations, then use an\nordinary `return` for a successful value. Do not mark a handler `async`.\n\n## Generated API\n\nCode generation writes browser references such as `api.todos.list` to `_generated/api.ts`. Web\ncode passes a reference to `useQuery`, `usePaginatedQuery`, or `useMutation`. The reference carries\nthe function's arguments, result, and errors into the call's TypeScript type.\n\n[Queries](queries.md) and [Mutations](mutations.md) cover each complete workflow.\n\n## Runtime boundary\n\nFunctions can import `_generated/server.ts`, `_generated/types.ts`, other server modules, shared\nmodules, and supported dependencies. They cannot import client files, `_generated/api.ts`,\n`ignotum/client`, Node built-ins, native add-ons, or dynamic imports.\n\nHosted functions have no direct network, filesystem, process environment, or process access. Use\nthe APIs provided through the function context. Typed server configuration is available through\n`ctx.env`; see [Environment variables](environment-variables.md).\n\nSee [Values](values.md) for validators, [Errors](errors.md) for the typed error model, and\n[Limits](limits.md) for execution and data limits.\n\nNext: [Queries](queries.md)\n"],
34936
35660
  ["generated-code.md", "---\ntitle: Generated code\ndescription: Understand generated function references, builders, and data types.\nsection: Development\nslug: generated-code\norder: 61\n---\n\n# Generated code\n\nIgnotum generates the bindings that connect the schema, functions, and Web code. The development\nserver updates them before startup and when the relevant source changes.\n\nRun generation directly before a typecheck or an editor session without the development server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nCode generation writes three files:\n\n| File | Contents |\n| ---------------------- | ------------------------------------------------------- |\n| `_generated/server.ts` | Schema-bound `query`, `mutation`, and `values` exports. |\n| `_generated/api.ts` | Web references such as `api.todos.list`. |\n| `_generated/types.ts` | `DataModel`, `Doc`, and `Id` types. |\n\nDo not edit these files. Ignotum refuses to overwrite a conflicting file that lacks its generated\nheader. Move the conflicting file elsewhere and run `codegen` again.\n\nKeep the `.js` suffix when importing a generated module:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Doc, Id } from \"@/_generated/types.js\";\n```\n\nThe API file changes when function modules or exports change. Server builders and data types change\nwith the schema. When `server/env.ts` exists, `_generated/server.ts` also types `ctx.env`; it does\nnot export environment values or `Secret`. See [Functions](functions.md) for function module naming\nrules.\n"],
34937
- ["guarantees.md", "---\ntitle: Guarantees\ndescription: Understand consistency, atomicity, ordering, retries, and realtime updates.\nsection: Reference\nslug: guarantees\norder: 70\n---\n\n# Guarantees\n\nThis page states the behavior application code can rely on. It also marks the boundaries where\nIgnotum deliberately makes no promise.\n\n## Queries read one committed state\n\nA completed query result corresponds to one committed app-state revision. It does not combine\nreads from different successful mutations.\n\nQueries may run while mutations are committing. If app data changes during a hosted query,\nIgnotum discards that attempt and runs it again. The hosted runtime makes at most four consistency\nattempts within 15 seconds. If changes keep preventing a stable result, the query fails instead of\nreturning a mixed result.\n\nQueries have no write access. A query result can be a success, a declared application error, or an\ninternal failure. All three are tied to the revision observed by that execution.\n\n## Mutations are atomic\n\nA mutation either commits all of its writes or none of them.\n\nIgnotum commits only after the handler returns a value that matches its return validator. These\noutcomes commit no writes:\n\n- a declared application error;\n- an uncaught `DocumentNotFound` or other application error;\n- a thrown exception;\n- an invalid return value or error value;\n- an execution, data, or storage limit failure;\n- an internal platform failure before commit.\n\nDocument changes, index changes, the mutation result used for retries, and the new app-state\nrevision become durable together. A caller that receives a successful mutation result can rely on\nthe writes having committed.\n\n## Mutations are serialized per app\n\nOnly one mutation changes a given app at a time. A mutation sees every mutation that committed\nbefore it began, and it sees its own earlier writes. No other mutation can interleave between its\nreads and writes.\n\nDifferent apps do not share this serialization. Queries may also execute while a mutation is\nrunning, subject to the consistent-query rule above.\n\nDo not infer an ordering between mutation calls started concurrently. If one business action must\nhappen after another, await the first mutation result before starting the second, or combine the\nsteps in one mutation.\n\n## Reconnect retries do not repeat a retained mutation\n\nEach browser mutation call has one invocation ID. The client retains the call while a connection\nis unavailable and sends the same ID, function, and arguments after a reconnect to the same\ndeployment.\n\nIgnotum records the first completed result for that invocation ID. A retry with the same input\nreturns that result without running the handler or applying its writes again. This applies to\nsuccessful mutations and declared application errors.\n\nA reused ID with different input is rejected. An invocation's input includes the deployment,\nfunction, operation kind, and arguments.\n\nHosted retry records last for seven days and are capped at the newest 10,000 results per app. The\ndeduplication guarantee ends when a record expires or is evicted. Use an application-level unique\nkey when duplicate prevention must last longer.\n\nThe client does not replay pending mutations after it detects a different deployment. It reloads\nthe page instead, because running an old call against new server code would change its meaning.\n\n## Realtime queries follow committed writes\n\nWhile a query remains subscribed and the connection can make progress, Ignotum refreshes it after\na successful mutation that may affect one of its reads. Failed mutations cause no refresh because\nthey changed no data.\n\nDependency tracking follows the database operation:\n\n| Read | Writes that may refresh it |\n| ---------------- | ----------------------------------------------------------------------- |\n| `find` or `get` | A change to that document. |\n| Full table query | A change in that table. |\n| Indexed query | A change whose old or new index position intersects the relevant range. |\n\nBounded indexed reads track the part of the range that can affect their result. Ignotum may refresh\na query whose returned value ends up unchanged. It favors an unnecessary rerun over missing a\nrelevant write.\n\nThe mutation promise does not wait for every affected query to render its next value. The commit\nis complete when the promise resolves, while query refresh and UI rendering continue separately.\n\nAfter a temporary disconnect, the browser keeps the latest settled query result. It recreates the\nsubscription after reconnecting. Ignotum checks recovered hosted query state against current app\nstate before serving it, so a commit that happened during recovery cannot make an older cached\nresult look current.\n\nThere is no delivery-time guarantee. A broken connection or unavailable runtime can delay an\nupdate, and infrastructure failures may reach the UI error boundary. Realtime queries are current\nresults, not an event log. Intermediate values may be coalesced when several mutations happen\nbefore a refresh completes.\n\n## Results do not move backward\n\nEach query snapshot carries the revision it observed. The client ignores a snapshot older than the\nlatest revision already accepted for that subscription. A slow earlier refresh cannot replace a\nnewer result.\n\nThe revision is an ordering value for complete query results. It is not exposed as an application\nevent number, and the public client API does not provide a change stream or resume token.\n\n## Pagination is not a frozen snapshot\n\nOne page is consistent in the same way as any other query result. Several pages loaded at different\ntimes do not share one historical snapshot.\n\nThe cursor records the query and the last returned position. It prevents using a cursor with a\ndifferent table, index, range, or direction. It does not freeze rows or index positions. If data is\ninserted, deleted, or reordered between page loads, an item can move across the cursor boundary.\n\nThe client subscribes to every loaded page, so earlier pages can also change while the list is\nopen. Render with document IDs and treat the combined list as a live ordered view.\n\n## Deployment activation is all or nothing\n\nA deployment contains its client and server code together. Ignotum changes the active deployment\nonly after the complete upload passes validation and any new indexes are ready. A build, upload,\nvalidation, or compatibility failure leaves the current deployment active.\n\nActivation reloads connected clients. A session never intentionally combines the client from one\ndeployment with functions from another.\n\n## What is not guaranteed\n\nIgnotum does not promise:\n\n- an order for mutation calls started concurrently;\n- permanent mutation deduplication beyond the published retention limits;\n- delivery of every intermediate query value;\n- a time bound for realtime updates or reconnects to succeed;\n- snapshot isolation across several pagination calls;\n- uniqueness for an application index;\n- uninterrupted hosted availability during a platform or persistent-data outage.\n\n[Limits](limits.md) lists the bounds that qualify these guarantees.\n"],
35661
+ ["guarantees.md", "---\ntitle: Guarantees\ndescription: Understand consistency, atomicity, ordering, retries, and realtime updates.\nsection: Reference\nslug: guarantees\norder: 70\n---\n\n# Guarantees\n\nThis page states the behavior application code can rely on. It also marks the boundaries where\nIgnotum deliberately makes no promise.\n\n## Queries read one committed state\n\nA completed query result corresponds to one committed app-state revision. It does not combine\nreads from different successful mutations.\n\nQueries may run while mutations are committing. If app data changes during a hosted query,\nIgnotum discards that attempt and runs it again. The hosted runtime makes at most four consistency\nattempts within 15 seconds. If changes keep preventing a stable result, the query fails instead of\nreturning a mixed result.\n\nQueries have no write access. A query result can be a success, a declared application error, or an\ninternal failure. All three are tied to the revision observed by that execution.\n\n## Mutations are atomic\n\nA mutation either commits all of its writes or none of them.\n\nIgnotum commits only after the handler returns a value that matches its return validator. These\noutcomes commit no writes:\n\n- a declared application error;\n- an uncaught `DocumentNotFound` or other application error;\n- a thrown exception;\n- an invalid return value or error value;\n- an execution, data, or storage limit failure;\n- an internal platform failure before commit.\n\nDocument changes, index changes, the mutation result used for retries, and the new app-state\nrevision become durable together. A caller that receives a successful mutation result can rely on\nthe writes having committed.\n\n## Mutations are serialized per app\n\nOnly one mutation changes a given app at a time. A mutation sees every mutation that committed\nbefore it began, and it sees its own earlier writes. No other mutation can interleave between its\nreads and writes.\n\nDifferent apps do not share this serialization. Queries may also execute while a mutation is\nrunning, subject to the consistent-query rule above.\n\nDo not infer an ordering between mutation calls started concurrently. If one business action must\nhappen after another, await the first mutation result before starting the second, or combine the\nsteps in one mutation.\n\n## Reconnect retries do not repeat a retained mutation\n\nEach browser mutation call has one invocation ID. The client retains the call while a connection\nis unavailable and sends the same ID, function, and arguments after a reconnect to the same\ndeployment.\n\nIgnotum records the first completed result for that invocation ID. A retry with the same input\nreturns that result without running the handler or applying its writes again. This applies to\nsuccessful mutations and declared application errors.\n\nA reused ID with different input is rejected. An invocation's input includes the app session, deployment,\nfunction, operation kind, and arguments.\n\nHosted retry records last for seven days and are capped at the newest 10,000 results per app. The\ndeduplication guarantee ends when a record expires or is evicted. Use an application-level unique\nkey when duplicate prevention must last longer.\n\nThe client does not replay pending mutations after it detects a different deployment. It reloads\nthe page instead, because running an old call against new server code would change its meaning.\n\n## Realtime queries follow committed writes\n\nWhile a query remains subscribed and the connection can make progress, Ignotum refreshes it after\na successful mutation that may affect one of its reads. Failed mutations cause no refresh because\nthey changed no data.\n\nDependency tracking follows the database operation:\n\n| Read | Writes that may refresh it |\n| ---------------- | ----------------------------------------------------------------------- |\n| `find` or `get` | A change to that document. |\n| Full table query | A change in that table. |\n| Indexed query | A change whose old or new index position intersects the relevant range. |\n\nBounded indexed reads track the part of the range that can affect their result. Ignotum may refresh\na query whose returned value ends up unchanged. It favors an unnecessary rerun over missing a\nrelevant write.\n\nThe mutation promise does not wait for every affected query to render its next value. The commit\nis complete when the promise resolves, while query refresh and UI rendering continue separately.\n\nAfter a temporary disconnect, the browser keeps the latest settled query result. It recreates the\nsubscription after reconnecting. Ignotum checks recovered hosted query state against current app\nstate before serving it, so a commit that happened during recovery cannot make an older cached\nresult look current.\n\nRecovering an idle hosted connection does not resend cached query results already sent to its\nsubscriptions. A new subscription still receives its initial result. Query refreshes may emit the\nsame value again, so subscription callbacks should not be treated as notifications of data changes.\n\nThere is no delivery-time guarantee. A broken connection or unavailable runtime can delay an\nupdate, and infrastructure failures may reach the UI error boundary. Realtime queries are current\nresults, not an event log. Intermediate values may be coalesced when several mutations happen\nbefore a refresh completes.\n\n## Results do not move backward\n\nEach query snapshot carries the revision it observed. The client ignores a snapshot older than the\nlatest revision already accepted for that subscription. A slow earlier refresh cannot replace a\nnewer result.\n\nThe revision is an ordering value for complete query results. It is not exposed as an application\nevent number, and the public client API does not provide a change stream or resume token.\n\n## Pagination is not a frozen snapshot\n\nOne page is consistent in the same way as any other query result. Several pages loaded at different\ntimes do not share one historical snapshot.\n\nThe cursor records the query and the last returned position. It prevents using a cursor with a\ndifferent table, index, range, or direction. It does not freeze rows or index positions. If data is\ninserted, deleted, or reordered between page loads, an item can move across the cursor boundary.\n\nThe client subscribes to every loaded page, so earlier pages can also change while the list is\nopen. Render with document IDs and treat the combined list as a live ordered view.\n\n## Deployment activation is all or nothing\n\nA deployment contains its client and server code together. Ignotum changes the active deployment\nonly after the complete upload passes validation and any new indexes are ready. A build, upload,\nvalidation, or compatibility failure leaves the current deployment active.\n\nActivation reloads connected clients. A session never intentionally combines the client from one\ndeployment with functions from another.\n\n## What is not guaranteed\n\nIgnotum does not promise:\n\n- an order for mutation calls started concurrently;\n- permanent mutation deduplication beyond the published retention limits;\n- delivery of every intermediate query value;\n- a time bound for realtime updates or reconnects to succeed;\n- snapshot isolation across several pagination calls;\n- uniqueness for an application index;\n- uninterrupted hosted availability during a platform or persistent-data outage.\n\n[Limits](limits.md) lists the bounds that qualify these guarantees.\n"],
34938
35662
  ["how-ignotum-works.md", "---\ntitle: How Ignotum works\ndescription: Understand how data, functions, files, Web code, and deployments fit together.\nsection: Start\nslug: how-ignotum-works\norder: 3\n---\n\n# How Ignotum works\n\nAn Ignotum app is one project with a database, functions, application files, and a browser-facing\nWeb part. A deployment publishes all of them together.\n\n## Database and functions\n\nThe schema names the app's tables and describes every stored field. Queries read the database.\nMutations read and write it in atomic operations.\n\nEach exported query or mutation receives a generated reference. Web code uses that reference\ninstead of a handwritten route or request type. The function's argument, result, and application\nerror definitions determine the corresponding TypeScript types in the browser.\n\n## Realtime results\n\nCalling a query from Web code creates a subscription. Ignotum records the database reads made by\nthe query and refreshes it after a committed mutation may have changed those reads. The UI receives\nthe current complete result rather than a stream of database operations.\n\nSeveral components using the same query and arguments share one browser subscription. A temporary\ndisconnect keeps the latest settled result while the client reconnects and recreates active\nsubscriptions.\n\n## Files\n\nApplication files are values that can appear in mutation arguments and database documents. Web\ncode passes a browser `File` to a mutation. The mutation stores the validated file value in a\ndocument, and a query can return it to Web code with a URL for the current query result.\n\nFiles in the top-level `public` directory are different. They are fixed deployment assets with\nstable paths and do not belong to database documents.\n\n## Web\n\nThe Web part starts at `client/index.tsx`. Its default `app(...)` export supplies the page title and\nroot Preact component. It calls functions through generated references and renders their typed\nresults.\n\n## Deployment\n\n`ignotum deploy` publishes the Web code, functions, schema, and public assets as one deployment.\nActivation changes them together. Existing browser sessions reload when they detect the new\ndeployment.\n\nNext: [Project structure](project-structure.md)\n"],
34939
35663
  ["index.md", "---\ntitle: Ignotum\nlabel: Introduction\ndescription: Learn the application model used by Ignotum.\nsection: Start\nslug: /\norder: 1\n---\n\n# Ignotum\n\n> Ignotum is currently in private alpha. APIs and hosted service behavior may change before public\n> availability.\n\nIgnotum is an application cloud for TypeScript apps. An app defines its data, callable functions,\nbrowser UI, and files in one project. The CLI runs the project locally and deploys it as one unit.\n\nThe database stores application data. Queries read that data, mutations change it, and generated\nreferences let the Web part call both with matching TypeScript types. Active queries receive new\nresults after relevant mutations commit.\n\nStart with the [Quickstart](quickstart.md). It creates a small app and follows the path from schema\nto deployment. [How Ignotum works](how-ignotum-works.md) explains how the parts fit together, and\n[Project structure](project-structure.md) explains the `client`, `server`, and `shared` directories.\n\nNext: [Quickstart](quickstart.md)\n"],
34940
35664
  ["indexes.md", "---\ntitle: Indexes\ndescription: Define indexes and use them for ranges and ordering.\nsection: Database\nslug: indexes\norder: 15\n---\n\n# Indexes\n\nAn index gives documents an order that functions can select and bound. Declare each index with its\ntable in `server/schema.ts`:\n\n```ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]),\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters. `by_channel_priority` supports a range for one channel, or for one channel and its\npriorities. It is not a priority-only index.\n\nAn index name must be non-empty, cannot begin with `$`, and is limited to 64 UTF-8 bytes. A table\ncannot repeat a name, repeat a field inside one index, or declare two indexes with the same fields\nin the same order. See [Limits](limits.md) for index counts and key size.\n\n## Select an index\n\nUse the declared name in a function:\n\n```ts\nconst todos =\n yield *\n ctx.db\n .query(\"todos\")\n .index(\"by_channel_priority\", (range) =>\n range.eq(\"channel\", args.channel).gte(\"priority\", args.minimumPriority),\n )\n .collect();\n```\n\nThe range builder follows the declared field order. Match any number of leading fields with `eq`,\nthen optionally bound the next field:\n\n| Method | Bound |\n| ------------------- | ------------------------------------------------- |\n| `eq(field, value)` | Equal to `value`. May continue to the next field. |\n| `gt(field, value)` | Greater than `value`. |\n| `gte(field, value)` | Greater than or equal to `value`. |\n| `lt(field, value)` | Less than `value`. |\n| `lte(field, value)` | Less than or equal to `value`. |\n\nChain one lower and one upper bound on the same field for a two-sided range:\n\n```ts\nconst thisWeek =\n yield *\n ctx.db\n .query(\"events\")\n .index(\"by_workspace_start\", (range) =>\n range\n .eq(\"workspaceId\", args.workspaceId)\n .gte(\"start\", args.weekStart)\n .lt(\"start\", args.nextWeek),\n )\n .collect();\n```\n\nCalling `index` without a range reads the whole index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders equal declared values by `createdAt`, then `id`. After matching all declared\nfields with `eq`, those fields are also available to the range builder.\n\n`order(\"desc\")` reverses the complete index order. It cannot sort by a field outside the selected\nindex.\n\nUse [Pagination](pagination.md) for large or incremental index results.\n"],
34941
- ["limits.md", "---\ntitle: Limits\ndescription: Understand hosted limits for functions, data, connections, and deployments.\nsection: Reference\nslug: limits\norder: 71\n---\n\n# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports sync admission, execution time, result size, and app storage quota failures as\n`ResourceLimitExceeded`. Some lower-level document, collection, memory, stack, or operation limit\nfailures become `InternalServerError` instead. A deployment that exceeds a limit fails before\nactivation, so the current deployment stays active.\n\n## Functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nOne server environment may contain at most 128 variables and 64 KiB of values as JSON.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries a query when app data changes while it runs. One logical execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nThe inventory contains at most 512 listed files. Its own `inventory.json` upload is additional.\nRequests with JSON bodies to `api.ignotum.cloud` are also limited to 1 MiB.\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
34942
- ["local-development.md", "---\ntitle: Local development\ndescription: Run an Ignotum app locally and manage its local data.\nsection: Development\nslug: local-development\norder: 60\n---\n\n# Local development\n\nRun the development server from the app directory:\n\n```sh\nnpx ignotum dev\n```\n\nThe command requires `client/index.tsx` and `server/schema.ts`. It updates generated code, starts\nthe Web and function runtimes, and prints the local URL. The default is\n<http://127.0.0.1:3210>.\n\nClient changes use hot reload. Changes to server `.ts` files reload the function modules and\nrefresh active queries. Adding or removing a function module also updates the API references.\nChanges to `.env.ignotum` validate and reload `ctx.env`, then refresh active queries. Functions stay\nunavailable after an invalid edit until the file is fixed.\n\nThe development server uses one local database for the app directory. A second server for the same\napp fails with a message that identifies the process holding the database lock.\n\n## Options\n\nUse flags to change the listener or open a browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------------------------------------ |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. The command fails if it is in use. |\n| `--open` | off | Open the app in the default browser after startup. |\n\n## Local data\n\nLocal data survives restarts and lives under `.ignotum/dev`. The generated project ignores\n`.ignotum` in Git.\n\nStop the development server before resetting the database:\n\n```sh\nnpx ignotum dev db reset\n```\n\nThe command refuses to reset data while a live development server holds the lock. A successful\nreset removes the local database. The next start creates an empty database from the current schema.\n\nLocal development does not reproduce every hosted resource limit or the hosted function runtime.\nRun the typecheck and review [Limits](limits.md) before deployment.\n\nNext: [Generated code](generated-code.md)\n"],
34943
- ["mutations.md", "---\ntitle: Mutations\ndescription: Define and call typed mutations.\nsection: Functions\nslug: mutations\norder: 23\n---\n\n# Mutations\n\nA mutation reads and changes application data in one atomic operation. Web code calls it through a\ngenerated reference and receives a typed value or application error.\n\n## Define a mutation\n\nImport `mutation` and schema-bound `values` from `_generated/server.ts`:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const create = mutation({\n args: { text: values.string() },\n returns: values.id(\"todos\"),\n\n handler: function* (ctx, { text }) {\n return yield* ctx.db.insert(\"todos\", {\n text,\n completed: false,\n });\n },\n});\n```\n\nMutation `ctx.db` includes all read methods plus `insert`, `patch`, `replace`, and `delete`. See\n[Writing data](writing-data.md) for their exact behavior.\n\nAll reads and writes in one handler belong to one transaction. A successful return commits them\ntogether. An application error, thrown exception, invalid return, limit failure, or platform\nfailure commits none of them. Other mutations for the same app do not interleave with the handler.\n\n## Call a mutation from Web code\n\n`useMutation` takes the generated reference and returns a stable typed function:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\n\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe promise resolves with a settled `Result` after the mutation commits or returns a declared\napplication error. It rejects for client or protocol failures, including unsupported argument\nvalues, an unavailable function, or a hosted limit that prevents the call from starting.\n\n## Handle the result\n\nThe client `Result` export contains `match`. Server operations such as `Result.fail`,\n`Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available in browser code.\n\nApplication errors use the `error` handler. A single function receives their union. An object must\nhandle every `_tag` separately. The optional `internalError` handler receives an internal failure's\nrequest ID. Without that handler, `Result.match` throws the error so an error boundary can handle\nit. See [Errors](errors.md).\n\n## Reconnects and retries\n\nThe client assigns one invocation ID to a mutation call. If the connection drops before the result\narrives, it keeps the promise pending and sends the same invocation after a matching reconnect.\nIgnotum returns a retained result instead of applying the mutation twice.\n\nA deployment change reloads the page and does not replay pending work against different function\ncode. Retry records have retention limits and are not permanent duplicate protection for business\noperations. See [Guarantees](guarantees.md) and [Limits](limits.md).\n\nSuccessful mutations can refresh active queries after the commit. The mutation result can reach\nits caller before every affected query renders its next value.\n\nNext: [Files](files.md)\n"],
35665
+ ["limits.md", "---\ntitle: Limits\ndescription: Understand hosted limits for functions, data, connections, and deployments.\nsection: Reference\nslug: limits\norder: 71\n---\n\n# Limits\n\nThese limits apply to hosted Ignotum apps. The local dev server does not reproduce every hosted\nlimit, so an operation that works locally can still be rejected after deployment.\n\nIgnotum reports sync admission, execution time, result size, and app storage quota failures as\n`ResourceLimitExceeded`. Some lower-level document, collection, memory, stack, or operation limit\nfailures become `InternalServerError` instead. A deployment that exceeds a limit fails before\nactivation, so the current deployment stays active.\n\n## Functions\n\n| Limit | Value |\n| --------------------------------------- | ---------: |\n| Function arguments | 16 KiB |\n| Function result | 1 MiB |\n| Execution time | 10 seconds |\n| Memory | 32 MiB |\n| Stack | 512 KiB |\n| Ignotum operations during one execution | 1,000 |\n\nArgument and result sizes use their JSON representation. The result limit applies to successful\nresults and application errors.\n\nOne server environment may contain at most 128 variables and 64 KiB of values as JSON.\n\nAn Ignotum operation is a call through the function context, such as a database read or write. A\nfunction stops when it reaches the execution time or operation limit.\n\nIgnotum retries a query when app data changes while it runs. One logical execution makes at\nmost four attempts and stops after 15 seconds. The query fails temporarily if it cannot read a\nconsistent result in that time.\n\n## App data\n\n| Limit | Value |\n| --------------------------------------- | -------------: |\n| Stored fields in one document | 256 KiB |\n| Documents returned by `collect()` | 1,000 |\n| Documents returned by `take()` | 1,000 |\n| Documents returned by one page | 1,000 |\n| Document fields returned by `collect()` | 1 MiB |\n| Stored app data | 64 MiB per app |\n\nThe 1 MiB function-result limit still applies to `collect()`. Document IDs and timestamps take some\nspace in that result, so a collection can reach the result limit before its fields reach 1 MiB.\n\nIf a mutation would take the app over its storage limit, Ignotum rolls back the whole mutation.\nThe quota covers stored documents, application indexes, and the records Ignotum keeps to process\nmutations safely. Deployment files do not count as stored app data.\n\n## Schema indexes\n\n| Limit | Value |\n| ---------------------------- | -------------: |\n| Indexes on one table | 16 |\n| Indexes in one schema | 128 |\n| Declared fields in one index | 8 |\n| Index name | 64 UTF-8 bytes |\n| One encoded index key | 4 KiB |\n\nEvery index also includes `createdAt` and `id` after its declared fields. The key limit applies to\nthe combined encoded field values and those final ordering values. A document must fit every index\ndeclared for its table.\n\n## Realtime connections and calls\n\n| Limit | Value |\n| ------------------------- | ---------------------------: |\n| Live connections | 256 per app |\n| Incoming realtime message | 64 KiB |\n| Subscriptions | 128 per connection |\n| Active unique queries | 64 per app |\n| Realtime query refresh | 4 attempts within 15 seconds |\n| Concurrent operations | 32 per app |\n| Mutation execution | 1 at a time per app |\n| Unresolved mutations | 32 per app |\n| Mutation calls | 60 per minute per app |\n\nA unique query is one function and argument combination. Several components or browser tabs can\nsubscribe to the same unique query without using another unique-query slot.\n\nA realtime refresh can repeat a query execution when app data changes again during the refresh.\n\nThe unresolved-mutation limit protects calls whose outcome is not known yet, such as a call waiting\nfor a retry after a connection failure. Once Ignotum receives a final result, that call no longer\ncounts toward the limit.\n\nIgnotum runs mutations for the same app one at a time. Queries and other live calls can still use\nthe remaining concurrent-operation slots. When the app has no free slot, a new mutation call\nrejects with `ResourceLimitExceeded` so its promise does not stay pending.\n\n## Mutation retries\n\nThe client keeps the ID of a pending mutation and reuses it after a reconnect. Ignotum remembers up\nto 10,000 mutation results per app for seven days. During that period, a retry returns the recorded\nresult instead of running the mutation again.\n\nDo not treat this as permanent duplicate protection. A mutation may run again after its record has\nexpired or fallen outside the 10,000 most recent results.\n\n## Deployments\n\n| Limit | Value |\n| ------------------------------ | ------: |\n| Files listed in one deployment | 512 |\n| One listed file | 16 MiB |\n| Deployment inventory | 1 MiB |\n| Server files combined | 64 MiB |\n| Listed files combined | 128 MiB |\n\nThe inventory contains at most 512 listed files. Its own `inventory.json` upload is additional.\nRequests with JSON bodies to `api.ignotum.cloud` are also limited to 1 MiB.\n\nIgnotum never deletes the active deployment. It also protects the three newest deployments that\ncompleted successfully. An older inactive deployment becomes eligible for deletion after seven\ndays. An unfinished upload becomes eligible after 24 hours.\n"],
35666
+ ["local-development.md", "---\ntitle: Local development\ndescription: Run an Ignotum app locally and manage its local data.\nsection: Development\nslug: local-development\norder: 60\n---\n\n# Local development\n\nRun the development server from the app directory:\n\n```sh\nnpx ignotum dev\n```\n\nThe command requires `client/index.tsx` and `server/schema.ts`. It updates generated code, starts\nthe Web and function runtimes, and prints the local URL. The default is\n<http://127.0.0.1:3210>.\n\nClient changes use hot reload. Changes to server `.ts` files reload the function modules and\nrefresh active queries. Adding or removing a function module also updates the API references.\nChanges to `.env.ignotum` validate and reload `ctx.env`, then refresh active queries. Functions stay\nunavailable after an invalid edit until the file is fixed.\n\nThe development server uses one local database for the app directory. A second server for the same\napp fails with a message that identifies the process holding the database lock.\n\nSee [Test authentication locally](local-authentication.md) to try different users without a Google\naccount.\n\n## Options\n\nUse flags to change the listener or open a browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\n| Flag | Default | Meaning |\n| -------- | ----------- | ------------------------------------------------------------------ |\n| `--host` | `127.0.0.1` | Address used by the local server. |\n| `--port` | `3210` | TCP port from 1 through 65,535. The command fails if it is in use. |\n| `--open` | off | Open the app in the default browser after startup. |\n\n## Local data\n\nLocal data survives restarts and lives under `.ignotum/dev`. The generated project ignores\n`.ignotum` in Git.\n\nStop the development server before resetting the database:\n\n```sh\nnpx ignotum dev db reset\n```\n\nThe command refuses to reset data while a live development server holds the lock. A successful\nreset removes the local database. The next start creates an empty database from the current schema.\n\nLocal development does not reproduce every hosted resource limit or the hosted function runtime.\nRun the typecheck and review [Limits](limits.md) before deployment.\n\nNext: [Generated code](generated-code.md)\n"],
35667
+ ["migrations.md", "---\ntitle: Migrations\ndescription: Evolve stored data with automatic schema changes, defaults, renames, and conversions.\nsection: Database\nslug: migrations\norder: 12\n---\n\n# Migrations\n\nChange your schema in `server/schema.ts` and deploy it with your updated functions and Web code.\nIgnotum checks existing documents before activating the deployment. Changes that leave every\nstored document valid need no conversion code.\n\nFor example, you can add a table, add an optional field, widen a validator, or change indexes.\nTightening a validator also works when all existing values already satisfy it. Removing a table\nrequires it to be empty. Removing a field requires it to be absent from existing documents or\nexplicitly removed by a document conversion.\n\n## Add a field with a default\n\nUse `.default(value)` when a missing field should receive a value:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n priority: values.integer().default(0),\n }),\n}));\n```\n\nExisting documents without `priority` receive `0`. New inserts and replacements may omit it;\nreads always return a number. A default fills a missing value. It does not replace `null`, repair\nan invalid value, or overwrite an existing value. Omitting a field from a patch leaves it unchanged.\n\nUse a required validator with `.default()`, rather than wrapping it in `values.optional()`.\nDefaults must themselves satisfy the validator.\n\n## Rename and convert a field\n\nUse `.renameFrom()` to preserve a field's value under a new name. Add `.from()` when its old\nrepresentation also needs to change:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n todos: table({\n title: values.string().renameFrom(\"text\"),\n status: values\n .literals(\"open\", \"done\")\n .renameFrom(\"completed\")\n .from(values.boolean(), (completed) => (completed ? \"done\" : \"open\")),\n priority: values.integer().default(0),\n }).index(\"by_status\", [\"status\"]),\n}));\n```\n\nA stored `{ text: \"Buy milk\", completed: false }` becomes\n`{ title: \"Buy milk\", status: \"open\", priority: 0 }`. The document keeps its ID, creation time,\nand last update time. Indexes reflect the converted fields.\n\nThe first argument to `.from()` describes an older value. The callback receives that value and\nreturns its current representation. The result must satisfy the current validator. Values already\nvalid under the current validator are preserved, so a conversion does not run again on every deploy.\nOrdinary writes accept only the current representation.\n\nThe same helpers work inside objects, arrays, and records. A rename cannot reuse another current\nfield's name. If a document contains both a renamed field and its old name, deployment fails rather\nthan choosing which value to keep.\n\n## Reshape a document\n\nUse a table conversion when a change depends on several fields, or when you need to remove a field:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({ name: values.string() }).from(\n values.object({ firstName: values.string(), lastName: values.string() }),\n (previous) => ({ name: `${previous.firstName} ${previous.lastName}` }),\n ),\n}));\n```\n\nReturn the complete current set of application fields. Do not include `id`, `createdAt`, or\n`updatedAt`. Use `.from()` on `values.object()` to reshape an embedded object in the same way.\n\nYou can keep several `.from()` alternatives for different historical representations. Each\nalternative must convert directly to the current shape. If multiple historical validators match\na value, the migration fails. Keep the rules needed by every environment you still deploy to,\nincluding environments that have skipped releases.\n\n## Write conversion callbacks\n\nWrite synchronous, deterministic callbacks that depend only on their input. They may be retried.\nDo not read the clock, generate random values, call the database, read environment variables, or\nperform network requests. Each converted document must stay within the normal document size limit,\nand conversions have execution limits.\n\nThese helpers handle changes within a document. Use application mutations for changes that need\nother documents, external data, or a different meaning for values that already pass the current\nvalidator. A conversion also cannot attach a new file or change a file's metadata; use the file\nand mutation APIs for those operations.\n\nFor example, changing what a string means does not make existing strings invalid, and changing an\nID's target table does not move documents between tables. Plan those changes explicitly in your\napplication.\n\n## Deployment and recovery\n\nIgnotum pauses app operations while it validates the new schema and converts stored data. The new\ndeployment starts serving requests only after the data and indexes are ready. Connected clients\nreload for the new deployment.\n\nIf validation or conversion fails, the current deployment resumes and stored data remains unchanged.\nThe error identifies the document or schema change that needs attention. Update the schema or\nconversion and deploy again.\n\nIf activation is interrupted and its outcome cannot be confirmed, the app may remain paused. Use\nthe deployment ID printed by the CLI to finish the same activation:\n\n```sh\nnpx ignotum deploy --resume dep_your_deployment_id\n```\n\nResuming uses the already uploaded deployment, including its original environment values. It does\nnot rebuild local source. Reactivating older code must also satisfy the current stored data; Ignotum\ndoes not automatically reverse conversions.\n\n## Local development\n\nLocal development applies the same schema helpers to your local data when the updated schema is\nused. A failed migration leaves local documents unchanged. Exercise conversions with representative\nold data before deploying, including missing values and every historical representation you support.\n\nNext: [Queries](queries.md)\n"],
35668
+ ["mutations.md", "---\ntitle: Mutations\ndescription: Define and call typed mutations.\nsection: Functions\nslug: mutations\norder: 23\n---\n\n# Mutations\n\nA mutation reads and changes application data in one atomic operation. Web code calls it through a\ngenerated reference and receives a typed value or application error.\n\n## Define a mutation\n\nImport `mutation` and schema-bound `values` from `_generated/server.ts`:\n\n```ts\nimport { mutation, values } from \"@/_generated/server.js\";\n\nexport const create = mutation({\n args: { text: values.string() },\n returns: values.id(\"todos\"),\n\n handler: function* (ctx, { text }) {\n return yield* ctx.db.insert(\"todos\", {\n text,\n completed: false,\n });\n },\n});\n```\n\nMutation `ctx.db` includes all read methods plus `insert`, `patch`, `replace`, and `delete`. See\n[Writing data](writing-data.md) for their exact behavior.\n\nAll reads and writes in one handler belong to one transaction. A successful return commits them\ntogether. An application error, thrown exception, invalid return, limit failure, or platform\nfailure commits none of them. Other mutations for the same app do not interleave with the handler.\n\n## Call a mutation from Web code\n\n`useMutation` takes the generated reference and returns a stable typed function:\n\n```tsx\nimport { Result, useMutation } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\n\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe promise resolves with a settled `Result` after the mutation commits or returns a declared\napplication error. It rejects for client or protocol failures, including unsupported argument\nvalues, an unavailable function, or a hosted limit that prevents the call from starting.\n\n## Handle the result\n\nThe client `Result` export contains `match`. Server operations such as `Result.fail`,\n`Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available in browser code.\n\nApplication errors use the `error` handler. A single function receives their union. An object must\nhandle every `_tag` separately. The optional `internalError` handler receives an internal failure's\nrequest ID. Without that handler, `Result.match` throws the error so an error boundary can handle\nit. See [Errors](errors.md).\n\n## Reconnects and retries\n\nThe client assigns one invocation ID to a mutation call. If the connection drops before the result\narrives, it keeps the promise pending and sends the same invocation after a matching reconnect.\nIgnotum returns a retained result instead of applying the mutation twice. Duplicate or delayed\nresponses for a completed call do not interrupt live queries or other mutation calls.\n\nA deployment change reloads the page and does not replay pending work against different function\ncode. Retry records have retention limits and are not permanent duplicate protection for business\noperations. See [Guarantees](guarantees.md) and [Limits](limits.md).\n\nSuccessful mutations can refresh active queries after the commit. The mutation result can reach\nits caller before every affected query renders its next value.\n\nNext: [Files](files.md)\n"],
34944
35669
  ["pagination.md", "---\ntitle: Pagination\ndescription: Return cursor-based pages and load them from Web code.\nsection: Database\nslug: pagination\norder: 16\n---\n\n# Pagination\n\nA paginated query accepts `values.pagination()` and returns `values.page(...)`:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n args: {\n completed: values.boolean(),\n pagination: values.pagination(),\n },\n returns: values.page(Todo),\n\n handler: function* (ctx, { completed, pagination }) {\n return yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", completed))\n .order(\"desc\")\n .paginate(pagination);\n },\n});\n```\n\nThe result has `items` and `nextCursor`. A `null` cursor means the query was exhausted. Pass a\nnon-null cursor back with the same table, index, range, and order. A cursor from another query is\nrejected. Treat cursors as opaque strings.\n\nPage sizes are integers from 1 through 1,000. A page returns no more than the requested size.\n\n## Load pages in Web code\n\n`usePaginatedQuery` supplies the cursor and page size. Callers pass only the function's other\narguments:\n\n```tsx\nimport { Result, usePaginatedQuery } from \"ignotum/client\";\n\nconst events = usePaginatedQuery(api.events.list, {\n project: \"api\",\n level: \"error\",\n});\n\nreturn Result.match(events, {\n pending: () => <p>Loading...</p>,\n value: ({ items, loadMore, status }) => (\n <>\n {items.map((event) => (\n <p key={event.id}>{event.message}</p>\n ))}\n {status !== \"Exhausted\" && (\n <button disabled={status === \"LoadingMore\"} onClick={loadMore}>\n {status === \"LoadingMore\" ? \"Loading...\" : \"Load more\"}\n </button>\n )}\n </>\n ),\n});\n```\n\nThe default page size is 20. Pass an options object as the third argument to use a value from 1\nthrough 1,000:\n\n```tsx\nconst events = usePaginatedQuery(api.events.list, { project: \"api\" }, { pageSize: 50 });\n```\n\nIf pagination is the query's only argument, omit the second argument. `Query.skip` is accepted in\nplace of arguments and keeps the result pending.\n\nThe successful value has these fields:\n\n| Field | Meaning |\n| ------------ | --------------------------------------------- |\n| `items` | Items from every loaded page, in query order. |\n| `loadMore()` | Requests one more page. |\n| `status` | `CanLoadMore`, `LoadingMore`, or `Exhausted`. |\n\nEarlier items remain visible while another page loads. Changing the query arguments or page size\nstarts again at the first page.\n\n## Live page behavior\n\nPages do not freeze the database at the first page's revision. Each call reads current committed\nstate and continues after its cursor position. Data that moves across that position between calls\ncan appear on a different page or no longer appear in the remaining pages.\n\nEach loaded page remains subscribed. Use item IDs when rendering and allow items to move in a\nchanging list. See [Guarantees](guarantees.md) for the complete pagination contract.\n"],
34945
- ["preact.md", "---\ntitle: Preact\ndescription: Use the Preact components, hooks, events, and types exposed by Ignotum.\nsection: Web\nslug: preact\norder: 42\n---\n\n# Preact\n\n`ignotum/client` supplies Preact's JSX runtime. Import components, hooks, and types from this entry\npoint instead of adding a second UI runtime.\n\nThe main exports are:\n\n| Area | Exports |\n| ----------- | --------------------------------------------------------------------------------------- |\n| Elements | `Fragment`, `createElement`, `h`, `cloneElement`, `createContext`, `createRef` |\n| Components | `Component`, `isValidElement`, `toChildArray` |\n| Hooks | `useState`, `useEffect`, `useMemo`, `useCallback`, `useReducer`, `useRef`, `useContext` |\n| Other hooks | `useId`, `useLayoutEffect`, `useImperativeHandle`, `useDebugValue`, `useErrorBoundary` |\n\nThe entry point also exports Preact's component, event, JSX, ref, and hook types. TypeScript usually\ninfers them from JSX and hook calls.\n\nUse the JSX `class` attribute. Event handlers receive Preact's typed DOM events:\n\n```tsx\nimport { useState, type TargetedInputEvent } from \"ignotum/client\";\n\nfunction Search() {\n const [text, setText] = useState(\"\");\n\n const update = (event: TargetedInputEvent<HTMLInputElement>) => {\n setText(event.currentTarget.value);\n };\n\n return <input class=\"rounded border px-3 py-2\" value={text} onInput={update} />;\n}\n```\n\nIgnotum-specific browser exports include `app`, `Files`, `Query`, `Result`, `useQuery`,\n`usePaginatedQuery`, and `useMutation`. Their complete workflows are documented in\n[Files](files.md), [Queries](queries.md), and [Mutations](mutations.md).\n\nNext: [Styling](styling.md)\n"],
35670
+ ["preact.md", "---\ntitle: Preact\ndescription: Use the Preact components, hooks, events, and types exposed by Ignotum.\nsection: Web\nslug: preact\norder: 42\n---\n\n# Preact\n\n`ignotum/client` supplies Preact's JSX runtime. Import components, hooks, and types from this entry\npoint instead of adding a second UI runtime.\n\nThe main exports are:\n\n| Area | Exports |\n| ----------- | --------------------------------------------------------------------------------------------- |\n| Elements | `Fragment`, `createElement`, `h`, `cloneElement`, `createContext`, `createRef` |\n| Components | `Component`, `isValidElement`, `toChildArray` |\n| Hooks | `useState`, `useEffect`, `useMemo`, `useCallback`, `useReducer`, `useRef`, `useContext` |\n| Other hooks | `useElementId`, `useLayoutEffect`, `useImperativeHandle`, `useDebugValue`, `useErrorBoundary` |\n\nThe entry point also exports Preact's component, event, JSX, ref, and hook types. TypeScript usually\ninfers them from JSX and hook calls.\n\nUse the JSX `class` attribute. Event handlers receive Preact's typed DOM events:\n\n```tsx\nimport { useState, type TargetedInputEvent } from \"ignotum/client\";\n\nfunction Search() {\n const [text, setText] = useState(\"\");\n\n const update = (event: TargetedInputEvent<HTMLInputElement>) => {\n setText(event.currentTarget.value);\n };\n\n return <input class=\"rounded border px-3 py-2\" value={text} onInput={update} />;\n}\n```\n\nIgnotum-specific browser exports include `app`, `Files`, `Query`, `Result`, `useQuery`,\n`usePaginatedQuery`, and `useMutation`. Their complete workflows are documented in\n[Files](files.md), [Queries](queries.md), and [Mutations](mutations.md).\n\nNext: [Styling](styling.md)\n\n`useElementId` is Preact's hook for generating element IDs. `useId` reads the current\n[Ignotum ID account](authentication.md).\n"],
34946
35671
  ["project-structure.md", "---\ntitle: Project structure\ndescription: Place Web code, functions, data definitions, and shared code in an Ignotum app.\nsection: Start\nslug: project-structure\norder: 4\n---\n\n# Project structure\n\nAn Ignotum project separates browser code from function code while keeping both in one app:\n\n```text\nclient/\n index.tsx\nserver/\n env.ts\n schema.ts\n todos.ts\nshared/\n_generated/\npublic/\n```\n\nOnly `client/index.tsx` and `server/schema.ts` are required. The other paths appear when the app\nneeds them.\n\n## `client`\n\n`client` contains the Web part. Its `index.tsx` file default-exports one `app(...)` definition:\n\n```tsx\nimport { app } from \"ignotum/client\";\n\nfunction App() {\n return <main>My app</main>;\n}\n\nexport default app({\n title: \"My app\",\n component: App,\n});\n```\n\nClient modules import browser APIs and UI exports from `ignotum/client`. They may also import\n`_generated/api.ts`, `_generated/types.ts`, other client modules, shared modules, and\nbrowser-compatible dependencies. They cannot import server modules or `ignotum/server`.\n\n## `server`\n\n`server/schema.ts` defines the database schema. The optional `server/env.ts` declares typed,\nserver-only [environment variables](environment-variables.md). Other `.ts` files directly inside `server` may\nexport queries and mutations. Their file names become generated API modules. For example,\n`server/todos.ts` produces references such as `api.todos.list`.\n\nSchema definitions import `defineSchema` from `ignotum/server`. Functions import schema-bound\n`query`, `mutation`, and `values` from `@/_generated/server.js`. Server modules may import other\nserver modules, shared modules, generated server and data types, and supported dependencies. They\ncannot import client modules or `ignotum/client`.\n\n## `shared`\n\n`shared` contains code that is valid in both runtimes. Shared modules cannot depend on browser-only\nor function-only APIs. Put a type, validator-independent helper, or constant here only when both\nsides use it.\n\n## `_generated`\n\nIgnotum writes generated function references, schema-bound builders, and data types to\n`_generated`. Do not edit these files. See [Generated code](generated-code.md).\n\n## `public`\n\nThe optional top-level `public` directory contains fixed files that keep their relative paths as\nWeb URLs. It is separate from application files stored in database documents. See\n[Public assets](public-assets.md).\n\nNext: [Schema](schema.md)\n"],
34947
35672
  ["public-assets.md", "---\ntitle: Public assets\ndescription: Add a favicon and fixed files to the Web part.\nsection: Web\nslug: public-assets\norder: 44\n---\n\n# Public assets\n\nPublic assets belong to a deployment and keep stable URL paths. They are different from\n[application files](files.md), which belong to database documents and require a current query\nresult.\n\n## Favicon\n\nAdd `client/icon.svg` to use an SVG favicon. Ignotum discovers this exact path. Without the file,\nthe page has no favicon link.\n\nThe icon must be a regular file. Other SVG files cannot go in `public`, but imported SVG assets may\nstill be part of client code.\n\n## `public`\n\nPut files that need fixed URLs in a top-level `public` directory next to `client` and `server`:\n\n```text\npublic/\n images/\n logo.png\n manual.pdf\n```\n\nThese files become `/images/logo.png` and `/manual.pdf`. `client/public` is not supported.\n\nIgnotum accepts AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. It checks the file signature against the\nextension. Entries must be regular files or directories, not symbolic links. Each path segment may\ncontain letters, numbers, `.`, `_`, `~`, or `-`. The first segment cannot be `_ignotum`, which is\nreserved for platform paths.\n\nEach file is limited to 16 MiB, and all public files count toward the deployment limits. See\n[Limits](limits.md).\n"],
34948
- ["queries.md", "---\ntitle: Queries\ndescription: Define, call, and subscribe to typed queries.\nsection: Functions\nslug: queries\norder: 22\n---\n\n# Queries\n\nA query reads application data and returns a typed result. Web code calls it through a generated\nreference and stays subscribed to relevant data changes.\n\n## Define a query\n\nImport `query` and schema-bound `values` from `_generated/server.ts`. Define the arguments, return\nvalue, and handler together:\n\n```ts\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n args: { id: values.id(\"todos\") },\n returns: values.doc(\"todos\"),\n\n handler: function* (ctx, { id }) {\n return yield* ctx.db.get(\"todos\", id);\n },\n});\n```\n\nQueries have a read-only `ctx.db`. Use [Reading data](reading-data.md) for document and table reads,\nand [Indexes](indexes.md) for filtered and ordered results.\n\n## Call a query from Web code\n\nThe exported function above becomes `api.todos.get`. Pass that reference to `useQuery`:\n\n```tsx\nimport { Query, Result, useQuery } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction Todo({ id }: { id: Id<\"todos\"> | undefined }) {\n const todo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n\n return Result.match(todo, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value.text}</p>,\n error: {\n DocumentNotFound: () => <p>The todo no longer exists.</p>,\n },\n });\n}\n```\n\nArgument-free queries take only the reference:\n\n```tsx\nconst todos = useQuery(api.todos.list);\n```\n\nQueries with arguments require the second parameter. The generated reference supplies its type.\n`Query.skip` keeps the hook pending and opens no subscription. Changing from `Query.skip` to\narguments starts the query. Changing the arguments switches the subscription and returns pending\nuntil the new query has a result.\n\n## Handle the result\n\n`useQuery` first returns a pending `QueryResult`, then a value or application error.\n`Result.match` requires handlers for the states and declared errors in its type:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nUse one `error` function when the UI treats all application errors alike. Use an object keyed by\n`_tag` when errors need different handling. `internalError` is optional. Without it, an\n`InternalServerError` is thrown to the nearest UI error boundary. See [Errors](errors.md).\n\n## Realtime behavior\n\nThe hook keeps its subscription until the component stops using it or its reference and arguments\nchange. Components using the same query and arguments share one browser subscription.\n\nAfter a successful mutation, Ignotum refreshes active queries whose recorded reads may have\nchanged. A document lookup responds to writes for that document. An indexed range responds to\nchanges inside its relevant range. A full table query responds to writes in that table.\n\nA lost connection does not change a settled query back to pending. The client keeps the latest\nresult, reconnects with an increasing delay capped at five seconds, then recreates active\nsubscriptions. A deployment change reloads the page.\n\nUse [Pagination](pagination.md) for paginated query definitions and `usePaginatedQuery`. Read\n[Guarantees](guarantees.md) for consistency, reconnect, and update behavior.\n\nNext: [Mutations](mutations.md)\n"],
35673
+ ["queries.md", "---\ntitle: Queries\ndescription: Define, call, and subscribe to typed queries.\nsection: Functions\nslug: queries\norder: 22\n---\n\n# Queries\n\nA query reads application data and returns a typed result. Web code calls it through a generated\nreference and stays subscribed to relevant data changes.\n\n## Define a query\n\nImport `query` and schema-bound `values` from `_generated/server.ts`. Define the arguments, return\nvalue, and handler together:\n\n```ts\nimport { query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n args: { id: values.id(\"todos\") },\n returns: values.doc(\"todos\"),\n\n handler: function* (ctx, { id }) {\n return yield* ctx.db.get(\"todos\", id);\n },\n});\n```\n\nQueries have a read-only `ctx.db`. Use [Reading data](reading-data.md) for document and table reads,\nand [Indexes](indexes.md) for filtered and ordered results.\n\n## Call a query from Web code\n\nThe exported function above becomes `api.todos.get`. Pass that reference to `useQuery`:\n\n```tsx\nimport { Query, Result, useQuery } from \"ignotum/client\";\nimport { api } from \"@/_generated/api.js\";\nimport type { Id } from \"@/_generated/types.js\";\n\nfunction Todo({ id }: { id: Id<\"todos\"> | undefined }) {\n const todo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n\n return Result.match(todo, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value.text}</p>,\n error: {\n DocumentNotFound: () => <p>The todo no longer exists.</p>,\n },\n });\n}\n```\n\nArgument-free queries take only the reference:\n\n```tsx\nconst todos = useQuery(api.todos.list);\n```\n\nQueries with arguments require the second parameter. The generated reference supplies its type.\n`Query.skip` keeps the hook pending and opens no subscription. Changing from `Query.skip` to\narguments starts the query. Changing the arguments switches the subscription and returns pending\nuntil the new query has a result.\n\n## Handle the result\n\n`useQuery` first returns a pending `QueryResult`, then a value or application error.\n`Result.match` requires handlers for the states and declared errors in its type:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nUse one `error` function when the UI treats all application errors alike. Use an object keyed by\n`_tag` when errors need different handling. `internalError` is optional. Without it, an\n`InternalServerError` is thrown to the nearest UI error boundary. See [Errors](errors.md).\n\n## Realtime behavior\n\nThe hook keeps its subscription until the component stops using it or its reference and arguments\nchange. Components using the same query and arguments share one browser subscription.\n\nAfter a successful mutation, Ignotum refreshes active queries whose recorded reads may have\nchanged. A document lookup responds to writes for that document. An indexed range responds to\nchanges inside its relevant range. A full table query responds to writes in that table.\n\nA lost connection keeps settled public queries visible. Queries that read `ctx.id` return to\npending if their session can no longer be verified. The client keeps the latest\nresult, reconnects with an increasing delay capped at five seconds, then recreates active\nsubscriptions. A deployment change reloads the page.\n\nUse [Pagination](pagination.md) for paginated query definitions and `usePaginatedQuery`. Read\n[Guarantees](guarantees.md) for consistency, reconnect, and update behavior.\n\nNext: [Mutations](mutations.md)\n"],
34949
35674
  ["quickstart.md", "---\ntitle: Quickstart\ndescription: Create, understand, and deploy a small Ignotum app.\nsection: Start\nslug: quickstart\norder: 2\n---\n\n# Quickstart\n\nIgnotum requires Node.js 22.18 or newer. Create the generated counter app and start it:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The page shows a counter backed by an Ignotum database.\n\n## Find the app parts\n\nThe files used in this guide are:\n\n```text\nclient/\n index.tsx\nserver/\n counter.ts\n schema.ts\nshared/\n utils.ts\n_generated/\n```\n\n`server/schema.ts` defines stored data. `server/counter.ts` defines the query and mutation.\n`client/index.tsx` defines the Web app and calls those functions. `shared/utils.ts` is available to\nboth runtimes. Ignotum owns the files under `_generated`.\n\n## Define data\n\nThe generated schema has one table:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\nThe schema supplies runtime validation and the document types used by functions and Web code.\n\n## Define a query\n\n`server/counter.ts` exports `get`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n```\n\nCode generation gives this function the reference `api.counter.get`. The Web app subscribes to it\nwith `useQuery`:\n\n```tsx\nconst count = useQuery(api.counter.get);\n```\n\n`Result.match` renders the pending state and the returned value:\n\n```tsx\n{\n Result.match(count, {\n pending: () => <p>Loading...</p>,\n value: (value) => <p>{value}</p>,\n });\n}\n```\n\n## Define a mutation\n\nThe generated `increment` mutation either inserts the first counter document or updates the\nexisting one:\n\n```ts\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + 1;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nWeb code obtains a typed caller from the generated reference:\n\n```tsx\nconst increment = useMutation(api.counter.increment);\n\n<button type=\"button\" onClick={() => void increment()}>\n Increment\n</button>;\n```\n\nClick the button in one browser window. The mutation commits the new value, then the active query\nreceives an updated result. Open a second window to see both subscriptions follow the same data.\n\n## Deploy the app\n\nSign in with GitHub, then choose a globally available app slug:\n\n```sh\nnpx ignotum auth login\nnpx ignotum deploy --app my-app\n```\n\nThe command prints the hosted URL and links this directory to the hosted app. Later deployments use\nthe saved link:\n\n```sh\nnpx ignotum deploy\n```\n\nRead [Deploy an app](deploy-app.md) for login storage, slug rules, and deployment failure behavior.\n\nNext: [How Ignotum works](how-ignotum-works.md)\n"],
34950
35675
  ["reading-data.md", "---\ntitle: Reading data\ndescription: Read individual documents and table results.\nsection: Database\nslug: reading-data\norder: 13\n---\n\n# Reading data\n\nQuery and mutation handlers read documents through `ctx.db`. A read can name one document, scan a\ntable, or select a declared index. Ignotum does not turn an unindexed filter into a hidden table\nscan. Use an [index](indexes.md) for a filtered database read, or read the table and filter it when\nthe table is intentionally small.\n\n## One document\n\nUse `find` when absence is expected:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Doc<\"todos\"> | undefined\n```\n\nUse `get` when absence is an application error path:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Doc<\"todos\">\n```\n\nA missing `get` fails with `DocumentNotFound`. The error contains `table` and `id`. Catch it to\nrecover or map it to a public error. If it remains in the handler, it becomes part of that\nfunction's typed application errors.\n\nIDs are scoped to their tables at compile time. A `users` ID cannot be passed to a `todos` read.\n\n## A table\n\nStart a table query with `ctx.db.query`:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nA table query is ordered by `createdAt`, then `id`, both ascending. Reverse both fields with\n`order(\"desc\")`:\n\n```ts\nconst newest = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\nUse a table query when the function needs a table-wide result. Any write in that table can cause a\nsubscribed query that used the result to refresh.\n\n## Result methods\n\nFinish a table or index query with one method:\n\n| Method | Result |\n| ------------------- | ------------------------------------------------------------------------------ |\n| `collect()` | Every matching document, within hosted collection limits. |\n| `take(count)` | At most `count` documents. `count` may be 0 through 1,000. |\n| `first()` | The first document, or `undefined`. |\n| `unique()` | The only document, or `undefined`. More than one match is an internal failure. |\n| `paginate(options)` | One page and an opaque cursor. |\n\n`unique()` checks a result. It does not make the index unique and does not prevent duplicate field\nvalues. Use `first()` when multiple matches are valid.\n\nPrefer `take` or `first` when the function only needs a bounded result. `collect` is capped at\n1,000 documents when hosted and may hit byte limits first.\n\n## Reads inside a mutation\n\nA mutation sees writes it made earlier in the same handler, including through table and index\nqueries:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\nconst changed = yield * ctx.db.get(\"todos\", todo.id);\n```\n\nNo other mutation can interleave with those operations for the same app. See\n[Writing data](writing-data.md) and [Guarantees](guarantees.md).\n"],
34951
- ["schema.md", "---\ntitle: Schema\ndescription: Define tables, fields, IDs, and stored document types.\nsection: Database\nslug: schema\norder: 11\n---\n\n# Schema\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }).index(\"by_completed\", [\"completed\"]),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n`createdAt` is set when Ignotum inserts the document. `updatedAt` starts at the same time and\nadvances on every successful patch or replacement. Both are JavaScript `Date` values in function\nand Web code.\n\nChain `index` after a table to declare an ordered index. [Indexes](indexes.md) covers field rules,\nranges, and ordering.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n\n## Schema changes\n\nLocal development updates tables and indexes as the schema changes. Existing documents still have\nto match the current field validators when a function reads them.\n\nFor hosted apps, the first active deployment fixes table names, field names, and field validators.\nLater deployments may add, remove, rename, or reorder indexes. They may also change functions and\nWeb code. A deployment that changes the stored table or field schema is built and uploaded, but\nactivation fails with `SchemaIncompatible`, leaving the current deployment active.\n\nSchema migrations are not part of the current public API. Plan the first hosted schema with this\nrestriction in mind.\n\nNext: [Queries](queries.md)\n"],
35676
+ ["schema.md", "---\ntitle: Schema\ndescription: Define tables, fields, IDs, and stored document types.\nsection: Database\nslug: schema\norder: 11\n---\n\n# Schema\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }).index(\"by_completed\", [\"completed\"]),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n`createdAt` is set when Ignotum inserts the document. `updatedAt` starts at the same time and\nadvances on every successful patch or replacement. Both are JavaScript `Date` values in function\nand Web code.\n\nChain `index` after a table to declare an ordered index. [Indexes](indexes.md) covers field rules,\nranges, and ordering.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n\n## Schema changes\n\nIgnotum validates existing data when you change the schema. Add tables and optional fields or widen\nvalidators without conversion code. Use defaults, renames, and conversions for changes to stored\nvalues. Read [Migrations](migrations.md) for examples and deployment behavior.\n\nNext: [Migrations](migrations.md)\n"],
34952
35677
  ["styling.md", "---\ntitle: Styling\ndescription: Style Web code with Tailwind CSS or imported CSS files.\nsection: Web\nslug: styling\norder: 43\n---\n\n# Styling\n\nIgnotum loads Tailwind CSS automatically. An app does not need a Tailwind configuration file or a\nframework stylesheet.\n\nUse utility classes through JSX's `class` attribute:\n\n```tsx\nfunction App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nCustom CSS is an ordinary client module. Give the file any name and import it from client code:\n\n```tsx\nimport \"./calendar.css\";\n```\n\nNext: [Public assets](public-assets.md)\n"],
34953
- ["values.md", "---\ntitle: Values\ndescription: Use runtime validators and their matching TypeScript types.\nsection: Database\nslug: values\norder: 12\n---\n\n# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. The same definition supplies a TypeScript type and a runtime check. Do not substitute a\nTypeScript-only interface where Ignotum asks for a validator.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.file(options?)` | `FileValue` | Accepts configured image formats and a byte limit. See [Files](files.md). |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\nEnvironment declarations also provide `values.secret()` and `values.secret(value)`. These produce\nredacted values that require `Secret.value(...)` for explicit access. Secret validators are not\navailable in schemas or function input and output contracts. See\n[Environment variables](environment-variables.md).\n\nDates cross the Web and function boundary as `Date` instances. Ignotum rejects invalid dates.\n`values.number()` rejects `NaN`, `Infinity`, and `-Infinity`. Use `values.integer()` when fractional\nvalues should also be rejected.\n\nMutation arguments accept objects with ordinary fields and arrays. Convert built-in collections\nsuch as `Map` and `Set` to objects or arrays, and convert `URL` instances to strings before calling\na mutation. Ignotum rejects these built-in objects instead of discarding their contents.\n\n`values.file()` accepts JPEG, PNG, WebP, AVIF, and GIF by default. Pass `formats` to allow a\nnon-empty subset and `maxBytes` to set a positive limit up to 10 MiB. [Files](files.md) covers the\ncomplete file workflow and the places where file values are allowed.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method returns a new validator and leaves the source unchanged. The next method works from the\ncurrent result. Added fields can be picked, omitted, or overridden immediately. An omitted name can\nbe added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\nFixed object validators describe named fields. Use `values.record(value)` when keys are not known\nin advance.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n\nWrapping a validator in both supports omission and an explicit `null`:\n\n```ts\nvalues.optional(values.nullable(values.date()));\n```\n"],
35678
+ ["values.md", "---\ntitle: Values\ndescription: Use runtime validators and their matching TypeScript types.\nsection: Database\nslug: values\norder: 12\n---\n\n# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. The same definition supplies a TypeScript type and a runtime check. Do not substitute a\nTypeScript-only interface where Ignotum asks for a validator.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.file(options?)` | `FileValue` | Accepts configured image formats and a byte limit. See [Files](files.md). |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.userId()` | `UserId` | An app user ID returned by `ctx.id`. See [Authentication](authentication.md). |\n| `values.doc(\"todos\")` | `Doc<\"todos\">` | Accepts a complete document, including its `id`, `createdAt`, and `updatedAt` system fields. |\n| `values.pagination()` | Pagination options | Defines the `pagination` argument used by `.paginate(...)`. |\n| `values.page(value)` | A page of `T` | Defines the return value of a paginated query. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\nEnvironment declarations also provide `values.secret()` and `values.secret(value)`. These produce\nredacted values that require `Secret.value(...)` for explicit access. Secret validators are not\navailable in schemas or function input and output contracts. See\n[Environment variables](environment-variables.md).\n\nDates cross the Web and function boundary as `Date` instances. Ignotum rejects invalid dates.\n`values.number()` rejects `NaN`, `Infinity`, and `-Infinity`. Use `values.integer()` when fractional\nvalues should also be rejected.\n\nMutation arguments accept objects with ordinary fields and arrays. Convert built-in collections\nsuch as `Map` and `Set` to objects or arrays, and convert `URL` instances to strings before calling\na mutation. Ignotum rejects these built-in objects instead of discarding their contents.\n\n`values.file()` accepts JPEG, PNG, WebP, AVIF, and GIF by default. Pass `formats` to allow a\nnon-empty subset and `maxBytes` to set a positive limit up to 10 MiB. [Files](files.md) covers the\ncomplete file workflow and the places where file values are allowed.\n\n`values.doc`, `values.pagination`, and `values.page` are available on the schema-bound `values`\nexported by `_generated/server.ts`. They are not available while defining `server/schema.ts`.\n\n## Object transforms\n\nValidators created by `values.object(fields)` and `values.doc(\"table\")` support these chainable\nmethods:\n\n| Method | Result |\n| ------------------- | -------------------------------------------------------------------- |\n| `.pick(...keys)` | Keeps the listed fields. Every key must exist. |\n| `.omit(...keys)` | Removes the listed fields. Every key must exist. |\n| `.extend(fields)` | Adds fields. It rejects names that already exist. |\n| `.override(fields)` | Replaces validators for existing fields. It rejects new field names. |\n| `.partial()` | Makes every current field optional. |\n\nEach method returns a new validator and leaves the source unchanged. The next method works from the\ncurrent result. Added fields can be picked, omitted, or overridden immediately. An omitted name can\nbe added again with a different validator.\n\n```ts\nconst TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial().extend({\n requestId: values.string(),\n});\n```\n\nUse `override` when changing an existing field. This makes replacements visible in the definition\nand prevents `extend` from silently weakening fields such as `id`:\n\n```ts\nconst EditableTodo = values.doc(\"todos\").override({\n title: values.optional(values.string()),\n});\n```\n\nThese methods are only available on fixed object validators. Arrays, records, unions, errors, and\nprimitive validators do not expose them. After transforming a document validator, its inferred type\nmatches the current fields in the chain rather than the complete document type.\n\nFixed object validators describe named fields. Use `values.record(value)` when keys are not known\nin advance.\n\n### Reuse embedded objects in the schema\n\nCreate a fixed object validator inside the `defineSchema` callback when several stored fields share\nan object shape. A transform can derive a stored variant without repeating its fields:\n\n```ts\ndefineSchema(({ table, values }) => {\n const Contact = values.object({\n email: values.string(),\n phone: values.string(),\n });\n\n return {\n users: table({\n contact: Contact,\n }),\n publicProfiles: table({\n contact: Contact.omit(\"phone\"),\n }),\n };\n});\n```\n\nThis pattern suits embedded objects stored by more than one table, including full and reduced\nversions of the same object. The base validator stays inside `defineSchema`, where `values.id` can\ncheck its table references against the completed schema.\n\n### Derive server function validators from documents\n\nUse the schema-bound `values` from `_generated/server.ts` when a function input or output follows a\nstored document. These validators know every table name and the complete document fields, including\n`id`, `createdAt`, and `updatedAt`.\n\nReusable server validators can live in an ignored server module such as `server/_validators.ts`:\n\n```ts\nimport { values } from \"@/_generated/server.js\";\n\nexport const TodoInput = values.doc(\"todos\").omit(\"id\", \"createdAt\", \"updatedAt\").partial();\n\nexport const PublicTodo = values.doc(\"todos\").omit(\"updatedAt\");\n```\n\nImport these validators into queries and mutations that need the same contract. This works well for\npatch inputs and document projections. Do not import generated validators into `server/schema.ts`.\nDocument validators depend on the schema, so importing them while defining that schema would create\na cycle.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n```\n\nUse the other validators to define reusable values that do not represent a stored document:\n\n```ts\nconst TodoInput = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n\nWrapping a validator in both supports omission and an explicit `null`:\n\n```ts\nvalues.optional(values.nullable(values.date()));\n```\n\n## Schema evolution helpers\n\nUse `.default(value)` to fill an omitted field on creation and during migration, `.renameFrom(name)`\nto preserve a renamed field, and `.from(previousValidator, convert)` to convert a historical value.\nRead [Migrations](migrations.md) for examples, nested values, and document conversions.\n"],
34954
35679
  ["web.md", "---\ntitle: Web overview\nlabel: Overview\ndescription: Define the browser-facing part of an Ignotum app.\nsection: Web\nslug: web\norder: 40\n---\n\n# Web overview\n\nThe Web part of an Ignotum app lives in `client`. It renders the browser UI and calls queries and\nmutations through `_generated/api.ts`.\n\n`client/index.tsx` is the entry. It default-exports an `app(...)` definition with the page title and\nroot Preact component. Ignotum supplies the JSX runtime and Tailwind CSS.\n\n- [App entry](app-entry.md) documents `client/index.tsx` and the `app(...)` definition.\n- [Preact](preact.md) lists the UI exports and import rules.\n- [Styling](styling.md) covers Tailwind CSS and custom CSS.\n- [Public assets](public-assets.md) covers the favicon and files with fixed URL paths.\n- [Queries](queries.md) and [Mutations](mutations.md) cover the generated API and browser hooks.\n\nClient modules may import `_generated/api.ts`, `_generated/types.ts`, other client modules, shared\nmodules, and browser-compatible dependencies. They cannot import files under `server`,\n`_generated/server.ts`, `ignotum/server`, Node built-ins, or native add-ons.\n\nNext: [App entry](app-entry.md)\n"],
34955
35680
  ["writing-data.md", "---\ntitle: Writing data\ndescription: Insert, patch, replace, and delete documents in a mutation.\nsection: Database\nslug: writing-data\norder: 14\n---\n\n# Writing data\n\nOnly mutation handlers can write. Their `ctx.db` has four write methods and all methods from\n[Reading data](reading-data.md).\n\n## Insert\n\n`insert` creates a document and returns its table-specific ID:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n```\n\nSupply every required application field and omit optional fields that have no value. Ignotum sets\n`id`, `createdAt`, and `updatedAt`. Those system fields are not valid insert input.\n\n## Patch\n\n`patch` changes the supplied fields and leaves the rest alone:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nThe patch input is type-checked against the table, and every field is optional. A matching\ndocument keeps its `id` and `createdAt`; its `updatedAt` advances.\n\n## Replace\n\n`replace` supplies a new complete set of application fields:\n\n```ts\nyield *\n ctx.db.replace(\"todos\", args.id, {\n text: \"Build an app\",\n completed: false,\n });\n```\n\nEvery required field must be present. A matching document keeps its `id` and `createdAt`; its\n`updatedAt` advances.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Missing targets\n\n`patch`, `replace`, and `delete` fail with `DocumentNotFound` when the target document does not\nexist. The error contains the target `table` and `id`. Catch it directly or map it to a public\napplication error as described in [Errors](errors.md).\n\nUse `find` first when a missing target should be ignored:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\nif (todo !== undefined) yield * ctx.db.delete(\"todos\", args.id);\n```\n\n## Transaction behavior\n\nA mutation reads one logical state, sees its own earlier writes, and commits all successful writes\ntogether. Other mutations for the same app do not interleave with it.\n\n```ts\nexport const completeOldest = mutation({\n handler: function* (ctx) {\n const todo = yield* ctx.db\n .query(\"todos\")\n .index(\"by_completed\", (range) => range.eq(\"completed\", false))\n .first();\n\n if (todo !== undefined) {\n yield* ctx.db.patch(\"todos\", todo.id, { completed: true });\n }\n },\n});\n```\n\nThe commit happens only after the handler returns a valid success value. A declared application\nerror, thrown exception, invalid return, limit failure, or platform failure commits none of the\nmutation's writes.\n\nIgnotum validates stored fields and maintains declared indexes as part of the same atomic write.\nIf the mutation would exceed the hosted app storage quota, the whole mutation rolls back.\n\nSuccessful mutations cause active queries to refresh when their recorded reads may have changed.\nThe mutation result can reach its caller before every affected query has delivered its next\nsnapshot, so do not treat subscription rendering as part of the mutation promise. See\n[Guarantees](guarantees.md).\n"]
34956
35681
  ];
@@ -35382,9 +36107,20 @@ const dev = Command.make("dev", {
35382
36107
  userMessage: error.message
35383
36108
  })));
35384
36109
  })).pipe(Command.withDescription("Run the local Ignotum development server."), Command.withSubcommands([devDatabase]));
35385
- const deploy = Command.make("deploy", { app: Flag.string("app").pipe(Flag.withDescription("Create or link this app slug on the first deploy."), Flag.withSchema(AppSlug), Flag.optional) }, Effect.fn("deploy")(function* ({ app }) {
36110
+ const deploy = Command.make("deploy", {
36111
+ resume: Flag.string("resume").pipe(Flag.withDescription("Finish activation of an already uploaded deployment."), Flag.withSchema(DeploymentId), Flag.optional),
36112
+ app: Flag.string("app").pipe(Flag.withDescription("Create or link this app slug on the first deploy."), Flag.withSchema(AppSlug), Flag.optional)
36113
+ }, Effect.fn("deploy")(function* ({ app, resume }) {
35386
36114
  const path = yield* Path$1.Path;
35387
36115
  const terminal = yield* Terminal.Terminal;
36116
+ if (Option.isSome(resume)) {
36117
+ const result = yield* resumeDeployment(path.resolve("."), resume.value, Option.getOrUndefined(app)).pipe(Effect.provide(ControlClient.layer), Effect.mapError((error) => CliError.UserError.make({
36118
+ cause: error,
36119
+ userMessage: error.message
36120
+ })));
36121
+ yield* terminal.display(`Deployment activated.\n\nApp: ${result.appUrl}\nDeployment: ${result.deploymentId}\n`);
36122
+ return;
36123
+ }
35388
36124
  const result = yield* deploy$1(path.resolve("."), Option.getOrUndefined(app)).pipe(Effect.provide(ControlClient.layer), Effect.mapError((error) => CliError.UserError.make({
35389
36125
  cause: error,
35390
36126
  userMessage: error.message