ignotum 0.0.7 → 0.0.9

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 (58) hide show
  1. package/README.md +89 -101
  2. package/dist/cli/bin.mjs +1119 -195
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-DtX8qPrq.js → api-DzcR7spt.js} +36 -13
  5. package/dist/runtime/api-DzcR7spt.js.map +1 -0
  6. package/dist/runtime/{api-Dp4J-xt-.d.ts → api-jCl8Hzry.d.ts} +7 -9
  7. package/dist/runtime/client.d.ts +12 -5
  8. package/dist/runtime/client.js +285 -60
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{descriptor-t6BOEGw9-C1rwYIlx.js → descriptor-C5VA9qRl-BKbdenuU.js} +38 -7
  11. package/dist/runtime/descriptor-C5VA9qRl-BKbdenuU.js.map +1 -0
  12. package/dist/runtime/file-C1abuMgd.js +173 -0
  13. package/dist/runtime/file-C1abuMgd.js.map +1 -0
  14. package/dist/runtime/{id-Btwac71X-DhnKYsjY.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +51 -5
  15. package/dist/runtime/{index-B5KSOjGN.d.ts → index-D6VLTbDB.d.ts} +40 -21
  16. package/dist/runtime/internal/api.d.ts +2 -2
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +8 -7
  19. package/dist/runtime/internal/host.js +21 -10
  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 +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{pagination-B1BzNkh8-BUSTbeSg.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +8 -4
  26. package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
  27. package/dist/runtime/{schema-D9RmboaS.js → schema-B6PK_ZwV.js} +17 -3
  28. package/dist/runtime/schema-B6PK_ZwV.js.map +1 -0
  29. package/dist/runtime/server.d.ts +3 -3
  30. package/dist/runtime/server.js +2 -2
  31. package/dist/runtime/server.js.map +1 -1
  32. package/dist/runtime/sync-bMQq9tXx.d.ts +8 -0
  33. package/package.json +7 -5
  34. package/src/cli/agent-files.ts +52 -13
  35. package/src/cli/app-configuration.ts +4 -1
  36. package/src/cli/auth-client.ts +286 -0
  37. package/src/cli/build/server.ts +83 -6
  38. package/src/cli/command.ts +65 -2
  39. package/src/cli/control-client.ts +19 -6
  40. package/src/cli/new-app.ts +16 -0
  41. package/src/client/errors.ts +7 -10
  42. package/src/client/files.ts +153 -0
  43. package/src/client/hooks.ts +2 -15
  44. package/src/client/index.ts +7 -0
  45. package/src/client/sync.ts +158 -30
  46. package/src/dev-runtime/database.ts +69 -57
  47. package/src/dev-runtime/files.ts +337 -0
  48. package/src/dev-runtime/functions.ts +14 -6
  49. package/src/dev-runtime/migrations.ts +14 -0
  50. package/src/dev-runtime/sync.ts +134 -22
  51. package/src/internal/api.ts +16 -1
  52. package/src/server/index.ts +8 -1
  53. package/dist/runtime/api-DtX8qPrq.js.map +0 -1
  54. package/dist/runtime/descriptor-t6BOEGw9-C1rwYIlx.js.map +0 -1
  55. package/dist/runtime/id-D570vudg.js +0 -26
  56. package/dist/runtime/id-D570vudg.js.map +0 -1
  57. package/dist/runtime/pagination-BKPko9Hm.d.ts +0 -1
  58. package/dist/runtime/schema-D9RmboaS.js.map +0 -1
package/dist/cli/bin.mjs CHANGED
@@ -11,7 +11,7 @@ import * as Path$3 from "effect/Path";
11
11
  import { Path, TypeId } from "effect/Path";
12
12
  import * as Predicate$1 from "effect/Predicate";
13
13
  import * as Sink from "effect/Sink";
14
- import * as Stream from "effect/Stream";
14
+ import * as Stream$1 from "effect/Stream";
15
15
  import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
16
16
  import { ChildProcessSpawner, ExitCode, ProcessId, make, makeHandle } from "effect/unstable/process/ChildProcessSpawner";
17
17
  import * as NodeChildProcess from "node:child_process";
@@ -37,6 +37,7 @@ import * as NodeCrypto from "node:crypto";
37
37
  import * as Queue from "effect/Queue";
38
38
  import * as NFS from "node:fs";
39
39
  import * as OS from "node:os";
40
+ import { homedir } from "node:os";
40
41
  import * as Path$2 from "node:path";
41
42
  import * as Inspectable from "effect/Inspectable";
42
43
  import { pipeArguments } from "effect/Pipeable";
@@ -69,9 +70,11 @@ import * as RcRef from "effect/RcRef";
69
70
  import * as Terminal$1 from "effect/Terminal";
70
71
  import * as readline from "node:readline";
71
72
  import * as Runtime from "effect/Runtime";
72
- import { Array as Array$1, Brand, Cause, Config, 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, Ref, Result, Schedule, Schema, Semaphore, String as String$1, Terminal } from "effect";
73
+ import { Array as Array$1, Brand, Cause, Config, 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, Terminal } from "effect";
73
74
  import { Argument, CliError, Command, Flag, Prompt } from "effect/unstable/cli";
74
75
  import { customAlphabet } from "nanoid";
76
+ import { createAuthClient } from "better-auth/client";
77
+ import { deviceAuthorizationClient, organizationClient } from "better-auth/client/plugins";
75
78
  import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerRequest as HttpServerRequest$1, HttpServerResponse } from "effect/unstable/http";
76
79
  import tailwindcss from "@tailwindcss/vite";
77
80
  import { build, createServer, defaultClientConditions, defaultServerConditions, normalizePath } from "vite";
@@ -251,7 +254,7 @@ const pullIntoWritable = (options) => options.pull.pipe(Effect$1.flatMap((chunk)
251
254
  * @category constructors
252
255
  * @since 4.0.0
253
256
  */
254
- const fromReadable = (options) => Stream.fromChannel(fromReadableChannel(options));
257
+ const fromReadable = (options) => Stream$1.fromChannel(fromReadableChannel(options));
255
258
  /**
256
259
  * Creates a `Channel` that pulls chunks from a Node readable stream, mapping
257
260
  * errors with `onError` and destroying the readable on completion unless
@@ -406,7 +409,7 @@ var StreamAdapter = class extends Readable {
406
409
  constructor(context, stream) {
407
410
  super({});
408
411
  this.readLatch = Latch.makeUnsafe(false);
409
- this.fiber = Stream.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
412
+ this.fiber = Stream$1.runForEachArray(stream, (chunk) => this.readLatch.whenOpen(Effect$1.sync(() => {
410
413
  this.readLatch.closeUnsafe();
411
414
  for (let i = 0; i < chunk.length; i++) {
412
415
  const item = chunk[i];
@@ -462,7 +465,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
462
465
  ...options.env
463
466
  } : options.env;
464
467
  };
465
- const inputToStdioOption = (input) => Stream.isStream(input) ? "pipe" : input;
468
+ const inputToStdioOption = (input) => Stream$1.isStream(input) ? "pipe" : input;
466
469
  const outputToStdioOption = (input) => Sink.isSink(input) ? "pipe" : input;
467
470
  const resolveStdinOption = (options) => {
468
471
  const defaultConfig = {
@@ -475,7 +478,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
475
478
  ...defaultConfig,
476
479
  stream: options.stdin
477
480
  };
478
- if (Stream.isStream(options.stdin)) return {
481
+ if (Stream$1.isStream(options.stdin)) return {
479
482
  ...defaultConfig,
480
483
  stream: options.stdin
481
484
  };
@@ -519,7 +522,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
519
522
  const setupAdditionalFds = Effect$1.fnUntraced(function* (command, childProcess, additionalFds) {
520
523
  if (additionalFds.length === 0) return {
521
524
  getInputFd: () => Sink.drain,
522
- getOutputFd: () => Stream.empty
525
+ getOutputFd: () => Stream$1.empty
523
526
  };
524
527
  const inputSinks = /* @__PURE__ */ new Map();
525
528
  const outputStreams = /* @__PURE__ */ new Map();
@@ -532,12 +535,12 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
532
535
  evaluate: () => nodeStream,
533
536
  onError: (error) => toPlatformError(`fromWritable(fd${fd})`, toError(error), command)
534
537
  });
535
- if (config.stream) yield* Effect$1.forkScoped(Stream.run(config.stream, sink));
538
+ if (config.stream) yield* Effect$1.forkScoped(Stream$1.run(config.stream, sink));
536
539
  inputSinks.set(fd, sink);
537
540
  break;
538
541
  }
539
542
  case "output": {
540
- let stream = Stream.empty;
543
+ let stream = Stream$1.empty;
541
544
  if (nodeStream && "read" in nodeStream) {
542
545
  const passThrough = new PassThrough();
543
546
  nodeStream.on("error", (error) => passThrough.destroy(error));
@@ -547,7 +550,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
547
550
  onError: (error) => toPlatformError(`fromReadable(fd${fd})`, toError(error), command)
548
551
  });
549
552
  }
550
- if (config.sink) stream = Stream.transduce(stream, config.sink);
553
+ if (config.sink) stream = Stream$1.transduce(stream, config.sink);
551
554
  outputStreams.set(fd, stream);
552
555
  break;
553
556
  }
@@ -555,7 +558,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
555
558
  }
556
559
  return {
557
560
  getInputFd: (fd) => inputSinks.get(fd) ?? Sink.drain,
558
- getOutputFd: (fd) => outputStreams.get(fd) ?? Stream.empty
561
+ getOutputFd: (fd) => outputStreams.get(fd) ?? Stream$1.empty
559
562
  };
560
563
  });
561
564
  const setupChildStdin = (command, childProcess, config) => Effect$1.suspend(() => {
@@ -566,7 +569,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
566
569
  endOnDone: config.endOnDone,
567
570
  encoding: config.encoding
568
571
  });
569
- if (Stream.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream.run(config.stream, sink)), sink);
572
+ if (Stream$1.isStream(config.stream)) return Effect$1.as(Effect$1.forkScoped(Stream$1.run(config.stream, sink)), sink);
570
573
  return Effect$1.succeed(sink);
571
574
  });
572
575
  const setupChildOutputStreams = (command, childProcess, stdoutConfig, stderrConfig) => {
@@ -578,7 +581,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
578
581
  evaluate: () => passThrough,
579
582
  onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
580
583
  });
581
- })() : Stream.empty;
584
+ })() : Stream$1.empty;
582
585
  let stderr = childProcess.stderr ? (() => {
583
586
  const passThrough = new PassThrough();
584
587
  childProcess.stderr.on("error", (error) => passThrough.destroy(error));
@@ -587,10 +590,10 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
587
590
  evaluate: () => passThrough,
588
591
  onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
589
592
  });
590
- })() : Stream.empty;
591
- if (Sink.isSink(stdoutConfig.stream)) stdout = Stream.transduce(stdout, stdoutConfig.stream);
592
- if (Sink.isSink(stderrConfig.stream)) stderr = Stream.transduce(stderr, stderrConfig.stream);
593
- const all = Stream.merge(stdout, stderr);
593
+ })() : Stream$1.empty;
594
+ if (Sink.isSink(stdoutConfig.stream)) stdout = Stream$1.transduce(stdout, stdoutConfig.stream);
595
+ if (Sink.isSink(stderrConfig.stream)) stderr = Stream$1.transduce(stderr, stderrConfig.stream);
596
+ const all = Stream$1.merge(stdout, stderr);
594
597
  return {
595
598
  stdout,
596
599
  stderr,
@@ -744,7 +747,7 @@ const make$6 = /*#__PURE__*/ Effect$1.gen(function* () {
744
747
  const command = pipeline[i];
745
748
  const options = pipeOptions[i] ?? {};
746
749
  const stdinConfig = resolveStdinOption(command.options);
747
- const sourceStream = Stream.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
750
+ const sourceStream = Stream$1.unwrap(Effect$1.succeed(getSourceStream(handles[handles.length - 1], options.from)));
748
751
  const toOption = options.to ?? "stdin";
749
752
  if (toOption === "stdin") handles.push(yield* spawnCommand(ChildProcess$1.make(command.command, command.args, {
750
753
  ...command.options,
@@ -4759,7 +4762,7 @@ const utimes = /*#__PURE__*/ (() => {
4759
4762
  const nodeUtimes = /*#__PURE__*/ effectify(NFS.utimes, /*#__PURE__*/ handleErrnoException("FileSystem", "utime"), /*#__PURE__*/ handleBadArgument("utime"));
4760
4763
  return (path, atime, mtime) => nodeUtimes(path, atime, mtime);
4761
4764
  })();
4762
- const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
4765
+ const watchNode = (path, options) => Stream$1.callback((queue) => Effect$1.acquireRelease(Effect$1.sync(() => {
4763
4766
  const watcher = NFS.watch(path, { recursive: options?.recursive ?? false }, (event, path) => {
4764
4767
  if (!path) return;
4765
4768
  switch (event) {
@@ -4797,7 +4800,7 @@ const watchNode = (path, options) => Stream.callback((queue) => Effect$1.acquire
4797
4800
  });
4798
4801
  return watcher;
4799
4802
  }), (watcher) => Effect$1.sync(() => watcher.close())));
4800
- const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream.unwrap);
4803
+ const watch = (backend, path, options) => stat(path).pipe(Effect$1.map((stat) => backend.pipe(Option$1.flatMap((_) => _.register(path, stat, options)), Option$1.getOrElse(() => watchNode(path, options)))), Stream$1.unwrap);
4801
4804
  const writeFile$1 = (path, data, options) => Effect$1.callback((resume, signal) => {
4802
4805
  try {
4803
4806
  NFS.writeFile(path, data, {
@@ -28742,7 +28745,7 @@ const stream = (source, headers) => Multipart.makeConfig(headers).pipe(Effect$1.
28742
28745
  return parser;
28743
28746
  },
28744
28747
  onError: (error) => convertError(error)
28745
- })), Stream.unwrap, Stream.map(convertPart));
28748
+ })), Stream$1.unwrap, Stream$1.map(convertPart));
28746
28749
  /**
28747
28750
  * Parses multipart data from a Node readable request body and persists file
28748
28751
  * parts using the current `FileSystem`, `Path`, and `Scope` services.
@@ -29305,7 +29308,7 @@ const handleResponse = (request, response) => {
29305
29308
  nodeResponse.writeHead(response.status, headers);
29306
29309
  const drainLatch = Latch.makeUnsafe();
29307
29310
  nodeResponse.on("drain", () => drainLatch.openUnsafe());
29308
- return body.stream.pipe(Stream.orDie, Stream.runForEachArray((array) => {
29311
+ return body.stream.pipe(Stream$1.orDie, Stream$1.runForEachArray((array) => {
29309
29312
  const chunk = array.length > 1 ? Buffer.concat(array) : array[0];
29310
29313
  if (nodeResponse.write(chunk)) return Effect$1.void;
29311
29314
  drainLatch.closeUnsafe();
@@ -29396,6 +29399,13 @@ const DeploymentId = defineId("dep", "ignotum/hosted/DeploymentId");
29396
29399
  const TeamId = defineId("team", "ignotum/hosted/TeamId");
29397
29400
  const TableId = defineId("table", "ignotum/runtime/TableId");
29398
29401
  const PlatformPrincipalId = defineId("prn", "ignotum/control/PlatformPrincipalId");
29402
+ defineId("ses", "ignotum/auth/SessionId");
29403
+ defineId("acct", "ignotum/auth/AccountId");
29404
+ defineId("ver", "ignotum/auth/VerificationId");
29405
+ defineId("mbr", "ignotum/auth/MemberId");
29406
+ defineId("invt", "ignotum/auth/InvitationId");
29407
+ defineId("dvc", "ignotum/auth/DeviceCodeId");
29408
+ defineId("auth", "ignotum/auth/InternalId");
29399
29409
  const InvocationId = defineId("inv", "ignotum/sync/InvocationId");
29400
29410
  const SubscriptionId = defineId("sub", "ignotum/sync/SubscriptionId");
29401
29411
  defineId("conn", "ignotum/gateway/ConnectionId");
@@ -29407,6 +29417,124 @@ const DevDatabaseLockId = defineId("lock", "ignotum/dev/DatabaseLockId");
29407
29417
  const InvocationKey = Schema.String.pipe(Schema.brand("ignotum/hosted/InvocationKey"));
29408
29418
  const DeploymentGeneration = Schema.Natural.pipe(Schema.brand("ignotum/hosted/DeploymentGeneration"));
29409
29419
  const AppStateRevision = Schema.Natural.pipe(Schema.brand("ignotum/hosted/AppStateRevision"));
29420
+ const FileFormat = Schema.Literals([
29421
+ "jpeg",
29422
+ "png",
29423
+ "webp",
29424
+ "avif",
29425
+ "gif"
29426
+ ]);
29427
+ const fileMimeTypes = {
29428
+ avif: "image/avif",
29429
+ gif: "image/gif",
29430
+ jpeg: "image/jpeg",
29431
+ png: "image/png",
29432
+ webp: "image/webp"
29433
+ };
29434
+ const fileLimits = {
29435
+ activeGrantsPerConnection: 1024,
29436
+ distinctFilesPerQuerySnapshot: 256,
29437
+ fileBytes: 10485760,
29438
+ filesPerMutation: 8,
29439
+ filenameBytes: 255,
29440
+ mutationBytes: 26214400,
29441
+ preparationLifetimeMillis: 9e5,
29442
+ stagedBytesPerApp: 104857600
29443
+ };
29444
+ const FileId = Object.assign(Schema.String.pipe(Schema.check(Schema.isPattern(/^file_[0-9A-Za-z_-]{20,128}$/)), Schema.brand("ignotum/file/FileId")), { idPrefix: "file" });
29445
+ const FileUploadToken = defineId("upload", "ignotum/file/FileUploadToken");
29446
+ const FileGrantToken = defineId("grant", "ignotum/file/FileGrantToken");
29447
+ const FileValueTypeId = Symbol.for("ignotum/file/FileValue");
29448
+ const FileGrantTypeId = Symbol.for("ignotum/file/FileGrant");
29449
+ const isFileValue = (value) => Predicate.isObject(value) && Predicate.hasProperty(value, FileValueTypeId) && Predicate.isObject(value[FileValueTypeId]) && Predicate.hasProperty(value[FileValueTypeId], "id") && Schema.is(FileId)(value[FileValueTypeId].id) && Predicate.hasProperty(value, "format") && Schema.is(FileFormat)(value.format) && Predicate.hasProperty(value, "name") && Predicate.isString(value.name) && Predicate.hasProperty(value, "size") && Predicate.isNumber(value.size) && Predicate.hasProperty(value, "mimeType") && value.mimeType === fileMimeTypes[value.format];
29450
+ const buildFileValue = (id, metadata, grantUrl) => {
29451
+ const value = {
29452
+ format: metadata.format,
29453
+ mimeType: fileMimeTypes[metadata.format],
29454
+ name: metadata.name,
29455
+ size: metadata.size
29456
+ };
29457
+ Object.defineProperty(value, FileValueTypeId, {
29458
+ configurable: false,
29459
+ enumerable: false,
29460
+ value: Object.freeze({ id }),
29461
+ writable: false
29462
+ });
29463
+ if (grantUrl !== void 0) Object.defineProperty(value, FileGrantTypeId, {
29464
+ configurable: false,
29465
+ enumerable: false,
29466
+ value: grantUrl,
29467
+ writable: false
29468
+ });
29469
+ return Object.freeze(value);
29470
+ };
29471
+ const makeFileValue = (id, metadata) => buildFileValue(id, metadata);
29472
+ const fileIdOf = (value) => value[FileValueTypeId].id;
29473
+ const EncodedFileValue = Schema.Struct({
29474
+ $ignotum: Schema.Literal("file"),
29475
+ id: FileId,
29476
+ format: FileFormat,
29477
+ name: Schema.String,
29478
+ size: Schema.Int.check(Schema.isBetween({
29479
+ minimum: 0,
29480
+ maximum: fileLimits.fileBytes
29481
+ }))
29482
+ });
29483
+ const RuntimeFileMetadata = Schema.Struct({
29484
+ id: FileId,
29485
+ format: FileFormat,
29486
+ name: Schema.String,
29487
+ size: Schema.Int.check(Schema.isBetween({
29488
+ minimum: 0,
29489
+ maximum: fileLimits.fileBytes
29490
+ }))
29491
+ });
29492
+ const FilePath = Schema.Array(Schema.Union([Schema.String, Schema.Natural]));
29493
+ const RuntimeFileOccurrence = Schema.Struct({
29494
+ path: FilePath,
29495
+ file: RuntimeFileMetadata
29496
+ });
29497
+ const DecodedFileValue = Schema.declare(isFileValue, { title: "FileValue" });
29498
+ const FileValue = EncodedFileValue.pipe(Schema.decodeTo(DecodedFileValue, {
29499
+ decode: SchemaGetter.transform((encoded) => makeFileValue(encoded.id, {
29500
+ format: encoded.format,
29501
+ name: encoded.name,
29502
+ size: encoded.size
29503
+ })),
29504
+ encode: SchemaGetter.transform((value) => ({
29505
+ $ignotum: "file",
29506
+ format: value.format,
29507
+ id: fileIdOf(value),
29508
+ name: value.name,
29509
+ size: value.size
29510
+ }))
29511
+ }));
29512
+ const collectEncodedFileOccurrences = (value, path, occurrences) => {
29513
+ if (Schema.is(EncodedFileValue)(value)) {
29514
+ const file = Schema.decodeUnknownSync(EncodedFileValue)(value);
29515
+ occurrences.push({
29516
+ path,
29517
+ file: {
29518
+ format: file.format,
29519
+ id: file.id,
29520
+ name: file.name,
29521
+ size: file.size
29522
+ }
29523
+ });
29524
+ return;
29525
+ }
29526
+ if (globalThis.Array.isArray(value)) {
29527
+ for (const [index, child] of value.entries()) collectEncodedFileOccurrences(child, [...path, index], occurrences);
29528
+ return;
29529
+ }
29530
+ if (!Schema.is(Schema.JsonObject)(value)) return;
29531
+ for (const [key, child] of Object.entries(value)) collectEncodedFileOccurrences(child, [...path, key], occurrences);
29532
+ };
29533
+ const encodedFileOccurrencesOf = (value) => {
29534
+ const occurrences = [];
29535
+ collectEncodedFileOccurrences(value, [], occurrences);
29536
+ return occurrences;
29537
+ };
29410
29538
  //#endregion
29411
29539
  //#region ../contracts/dist/runtime/value.js
29412
29540
  const TransportValueSchema = Schema.suspend(() => Schema.Union([
@@ -29415,6 +29543,7 @@ const TransportValueSchema = Schema.suspend(() => Schema.Union([
29415
29543
  Schema.Finite,
29416
29544
  Schema.String,
29417
29545
  Schema.Date,
29546
+ FileValue,
29418
29547
  Schema.Array(TransportValueSchema),
29419
29548
  Schema.Record(Schema.String, Schema.UndefinedOr(TransportValueSchema))
29420
29549
  ]));
@@ -29424,6 +29553,7 @@ const collectDatePaths = (value, path, paths) => {
29424
29553
  paths.push(path);
29425
29554
  return;
29426
29555
  }
29556
+ if (isFileValue(value)) return;
29427
29557
  if (globalThis.Array.isArray(value)) {
29428
29558
  for (const [index, child] of value.entries()) collectDatePaths(child, [...path, index], paths);
29429
29559
  return;
@@ -29476,9 +29606,18 @@ const Invoke = Schema.Struct({
29476
29606
  function: FunctionAddress,
29477
29607
  args: Schema.Json
29478
29608
  });
29609
+ const Prepare = Schema.Struct({
29610
+ type: Schema.Literal("Prepare"),
29611
+ id: InvocationId,
29612
+ kind: Schema.Literal("Mutation"),
29613
+ function: FunctionAddress,
29614
+ args: Schema.Json,
29615
+ files: Schema.Array(FileId)
29616
+ });
29479
29617
  const ClientMessage = Schema.Union([
29480
29618
  Subscribe,
29481
29619
  Unsubscribe,
29620
+ Prepare,
29482
29621
  Invoke
29483
29622
  ]);
29484
29623
  const SubscriptionOperation = Schema.Struct({
@@ -29513,7 +29652,20 @@ const Snapshot = Schema.Struct({
29513
29652
  type: Schema.Literal("Snapshot"),
29514
29653
  id: SubscriptionId,
29515
29654
  result: WireResult,
29516
- revision: AppStateRevision
29655
+ revision: AppStateRevision,
29656
+ files: Schema.optional(Schema.Array(Schema.Struct({
29657
+ id: FileId,
29658
+ url: Schema.String
29659
+ })))
29660
+ });
29661
+ const Preparation = Schema.Struct({
29662
+ type: Schema.Literal("Preparation"),
29663
+ id: InvocationId,
29664
+ kind: Schema.Literal("Mutation"),
29665
+ uploads: Schema.Array(Schema.Struct({
29666
+ id: FileId,
29667
+ url: Schema.optional(Schema.String)
29668
+ }))
29517
29669
  });
29518
29670
  const SyncResultSuccess = Schema.Struct({
29519
29671
  type: Schema.Literal("Result"),
@@ -29526,7 +29678,7 @@ const SyncResultFailure = Schema.Struct({
29526
29678
  id: InvocationId,
29527
29679
  result: WireFailure
29528
29680
  });
29529
- const ProtocolErrorCode = Schema.Literals([
29681
+ const ErrorCode = Schema.Literals([
29530
29682
  "DuplicateOperationId",
29531
29683
  "FunctionUnavailable",
29532
29684
  "InvalidArguments",
@@ -29536,24 +29688,25 @@ const ProtocolErrorCode = Schema.Literals([
29536
29688
  "UnknownFunction",
29537
29689
  "WrongFunctionKind"
29538
29690
  ]);
29539
- const ProtocolError = Schema.Struct({
29540
- type: Schema.Literal("ProtocolError"),
29691
+ const SyncError = Schema.Struct({
29692
+ type: Schema.Literal("Error"),
29541
29693
  operation: Schema.optional(Operation),
29542
- code: ProtocolErrorCode,
29694
+ code: ErrorCode,
29543
29695
  message: Schema.String
29544
29696
  });
29545
- const DeploymentChanged = Schema.Struct({
29546
- type: Schema.Literal("DeploymentChanged"),
29697
+ const Deployment = Schema.Struct({
29698
+ type: Schema.Literal("Deployment"),
29547
29699
  deploymentId: DeploymentId,
29548
29700
  generation: DeploymentGeneration
29549
29701
  });
29550
29702
  const ServerMessage = Schema.Union([
29551
29703
  SyncHandshake,
29552
29704
  Snapshot,
29705
+ Preparation,
29553
29706
  SyncResultSuccess,
29554
29707
  SyncResultFailure,
29555
- ProtocolError,
29556
- DeploymentChanged
29708
+ SyncError,
29709
+ Deployment
29557
29710
  ]);
29558
29711
  const ClientMessageJson = Schema.fromJsonString(ClientMessage);
29559
29712
  const ServerMessageJson = Schema.fromJsonString(ServerMessage);
@@ -29580,7 +29733,7 @@ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailabl
29580
29733
  message: Schema.String
29581
29734
  }) {};
29582
29735
  //#endregion
29583
- //#region ../contracts/dist/descriptor-t6BOEGw9.js
29736
+ //#region ../contracts/dist/descriptor-C5VA9qRl.js
29584
29737
  const LiteralValue = Schema.Union([
29585
29738
  Schema.String,
29586
29739
  Schema.Finite,
@@ -29597,6 +29750,17 @@ const ValueDescriptor = Schema.Union([
29597
29750
  }),
29598
29751
  Schema.Struct({ type: Schema.Literal("boolean") }),
29599
29752
  Schema.Struct({ type: Schema.Literal("date") }),
29753
+ Schema.Struct({
29754
+ type: Schema.Literal("file"),
29755
+ formats: Schema.Array(Schema.Literals([
29756
+ "jpeg",
29757
+ "png",
29758
+ "webp",
29759
+ "avif",
29760
+ "gif"
29761
+ ])),
29762
+ maxBytes: Schema.Int
29763
+ }),
29600
29764
  Schema.Struct({
29601
29765
  type: Schema.Literal("error"),
29602
29766
  tag: Schema.String,
@@ -29654,6 +29818,49 @@ const getValueDescriptor = (value) => {
29654
29818
  if (!Predicate.hasProperty(value, ValueDescriptorTypeId)) return void 0;
29655
29819
  return Schema.is(ValueDescriptor)(value[ValueDescriptorTypeId]) ? value[ValueDescriptorTypeId] : void 0;
29656
29820
  };
29821
+ const descriptorContainsFile = (descriptor) => {
29822
+ switch (descriptor.type) {
29823
+ case "file": return true;
29824
+ case "array":
29825
+ case "nullable":
29826
+ case "optional":
29827
+ case "record": return descriptorContainsFile(descriptor.value);
29828
+ case "error":
29829
+ case "object": return descriptor.fields.some((field) => descriptorContainsFile(field.value));
29830
+ case "union": return descriptor.members.some(descriptorContainsFile);
29831
+ default: return false;
29832
+ }
29833
+ };
29834
+ const descriptorFields = (fields) => {
29835
+ const entries = [];
29836
+ for (const name of Reflect.ownKeys(fields)) {
29837
+ if (!Predicate.isString(name)) throw new Error("Ignotum value objects only support string field names.");
29838
+ const field = fields[name];
29839
+ const value = field === void 0 ? void 0 : getValueDescriptor(field);
29840
+ if (value === void 0) throw new Error(`The field '${name}' must use an Ignotum values validator.`);
29841
+ entries.push({
29842
+ name,
29843
+ value
29844
+ });
29845
+ }
29846
+ return entries.sort((left, right) => left.name.localeCompare(right.name));
29847
+ };
29848
+ //#endregion
29849
+ //#region ../contracts/dist/versioned.js
29850
+ /** Registers the first revision of a long-lived format. */
29851
+ const initial = (schema) => schema;
29852
+ /** Adds one adjacent migration and keeps encoding on the new current revision. */
29853
+ const upgrade = (previous, current, migrate) => {
29854
+ const accepted = Schema.Union([previous, current]);
29855
+ const isCurrent = Schema.is(current);
29856
+ return accepted.pipe(Schema.decodeTo(Schema.toType(current), {
29857
+ decode: SchemaGetter.transform((value) => {
29858
+ if (isCurrent(value)) return value;
29859
+ return migrate(value);
29860
+ }),
29861
+ encode: SchemaGetter.transform((value) => value)
29862
+ }));
29863
+ };
29657
29864
  //#endregion
29658
29865
  //#region ../contracts/dist/deployment.js
29659
29866
  const ArtifactPath = Schema.String.check(Schema.isPattern(/^(?!\/)(?![A-Za-z]:\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*(?:^|\/)\.(?:\/|$))(?!.*\/\/)[^\\\0]+$/)).pipe(Schema.brand("ignotum/deployment/ArtifactPath"));
@@ -29687,15 +29894,15 @@ const ClientRoute = Schema.Struct({
29687
29894
  pathname: ClientPath,
29688
29895
  artifact: ArtifactReference
29689
29896
  });
29690
- const DeploymentInventory = Schema.Struct({
29897
+ const DeploymentInventory = initial(Schema.Struct({
29691
29898
  formatVersion: Schema.Literal(1),
29692
29899
  files: Schema.Array(ArtifactFile)
29693
- });
29694
- const ClientManifest = Schema.Struct({
29900
+ }));
29901
+ const ClientManifest = initial(Schema.Struct({
29695
29902
  formatVersion: Schema.Literal(1),
29696
29903
  shell: ArtifactReference,
29697
29904
  routes: Schema.Array(ClientRoute)
29698
- });
29905
+ }));
29699
29906
  const ClientRoutingRoute = Schema.Struct({
29700
29907
  pathname: ClientPath,
29701
29908
  artifact: ArtifactPath
@@ -29713,26 +29920,60 @@ const SchemaSnapshotIndex = Schema.Struct({
29713
29920
  name: Schema.String,
29714
29921
  fields: Schema.Array(Schema.String)
29715
29922
  });
29923
+ const SchemaSnapshotTableV1 = Schema.Struct({
29924
+ name: Schema.String,
29925
+ fields: Schema.Array(SchemaSnapshotField)
29926
+ });
29927
+ const SchemaSnapshotV1 = Schema.Struct({
29928
+ formatVersion: Schema.Literal(1),
29929
+ tables: Schema.Array(SchemaSnapshotTableV1)
29930
+ });
29716
29931
  const SchemaSnapshotTable = Schema.Struct({
29717
29932
  name: Schema.String,
29718
29933
  fields: Schema.Array(SchemaSnapshotField),
29719
29934
  indexes: Schema.Array(SchemaSnapshotIndex)
29720
29935
  });
29721
- const SchemaSnapshot = Schema.Struct({
29936
+ const SchemaSnapshotV2 = Schema.Struct({
29722
29937
  formatVersion: Schema.Literal(2),
29723
29938
  tables: Schema.Array(SchemaSnapshotTable)
29724
29939
  });
29940
+ const SchemaSnapshot = upgrade(initial(SchemaSnapshotV1), SchemaSnapshotV2, (snapshot) => ({
29941
+ formatVersion: 2,
29942
+ tables: snapshot.tables.map((table) => ({
29943
+ ...table,
29944
+ indexes: []
29945
+ }))
29946
+ }));
29947
+ const ServerFunctionArtifactV1 = Schema.Struct({
29948
+ address: FunctionAddress,
29949
+ kind: FunctionKind,
29950
+ bundle: ArtifactReference,
29951
+ sourceMap: ArtifactReference
29952
+ });
29725
29953
  const ServerFunctionArtifact = Schema.Struct({
29726
29954
  address: FunctionAddress,
29727
29955
  kind: FunctionKind,
29956
+ args: Schema.optional(ValueDescriptor),
29957
+ returns: Schema.optional(ValueDescriptor),
29958
+ errors: Schema.optional(ValueDescriptor),
29728
29959
  bundle: ArtifactReference,
29729
29960
  sourceMap: ArtifactReference
29730
29961
  });
29731
- const ServerBuildManifest = Schema.Struct({
29962
+ const ServerBuildManifestV1 = Schema.Struct({
29732
29963
  formatVersion: Schema.Literal(1),
29733
29964
  schema: ArtifactReference,
29965
+ functions: Schema.Array(ServerFunctionArtifactV1)
29966
+ });
29967
+ const ServerBuildManifestV2 = Schema.Struct({
29968
+ formatVersion: Schema.Literal(2),
29969
+ schema: ArtifactReference,
29734
29970
  functions: Schema.Array(ServerFunctionArtifact)
29735
29971
  });
29972
+ const ServerBuildManifest = upgrade(initial(ServerBuildManifestV1), ServerBuildManifestV2, (manifest) => ({
29973
+ formatVersion: 2,
29974
+ schema: manifest.schema,
29975
+ functions: manifest.functions
29976
+ }));
29736
29977
  const deploymentInventoryPath = ArtifactPath.make("inventory.json");
29737
29978
  const clientManifestPath = ArtifactPath.make("client/manifest.json");
29738
29979
  const clientShellPath = ArtifactPath.make("client/shell.html");
@@ -29848,7 +30089,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
29848
30089
  };
29849
30090
  //#endregion
29850
30091
  //#region package.json
29851
- var version = "0.0.7";
30092
+ var version = "0.0.9";
29852
30093
  //#endregion
29853
30094
  //#region src/cli/codegen.ts
29854
30095
  const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
@@ -29976,6 +30217,183 @@ const generate = Effect.fn("Codegen.generate")(function* (appDirectory) {
29976
30217
  written
29977
30218
  };
29978
30219
  });
30220
+ const Credential = initial(Schema.Struct({
30221
+ formatVersion: Schema.Literal(1),
30222
+ authUrl: Schema.URLFromString,
30223
+ token: Schema.String
30224
+ }));
30225
+ const AuthUrl = Schema.URL.check(Schema.makeFilter((url) => url.protocol === "https:" || url.protocol === "http:" && [
30226
+ "127.0.0.1",
30227
+ "::1",
30228
+ "localhost"
30229
+ ].includes(url.hostname), { message: "The auth URL must use HTTPS unless it targets localhost." }));
30230
+ var AuthCommandError = class extends Schema.TaggedError()("AuthCommandError", {
30231
+ cause: Schema.optional(Schema.Defect()),
30232
+ message: Schema.String
30233
+ }) {};
30234
+ const commandError = (message, cause) => AuthCommandError.make(cause === void 0 ? { message } : {
30235
+ cause,
30236
+ message
30237
+ });
30238
+ var AuthCredentialStore = class AuthCredentialStore extends Context.Service()("ignotum/cli/auth-client/AuthCredentialStore") {
30239
+ static layer = Layer.effect(AuthCredentialStore, Effect.gen(function* () {
30240
+ const fileSystem = yield* FileSystem.FileSystem;
30241
+ const path = yield* Path$1.Path;
30242
+ const configured = yield* Config.option(Config.string("IGNOTUM_CONFIG_DIR"));
30243
+ const xdg = yield* Config.option(Config.string("XDG_CONFIG_HOME"));
30244
+ const appData = yield* Config.option(Config.string("APPDATA"));
30245
+ const configurationDirectory = Option.getOrElse(configured, () => Option.getOrElse(xdg, () => process$1.platform === "win32" && Option.isSome(appData) ? appData.value : path.join(homedir(), ".config")));
30246
+ const directory = path.join(configurationDirectory, "ignotum");
30247
+ const credentialPath = path.join(directory, "auth.json");
30248
+ const load = Effect.fn("AuthCredentialStore.load")(function* () {
30249
+ if (!(yield* fileSystem.exists(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not check the saved login.", cause))))) return;
30250
+ const text = yield* fileSystem.readFileString(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not read the saved login.", cause)));
30251
+ const credential = yield* Schema.decodeEffect(Schema.fromJsonString(Credential))(text).pipe(Effect.mapError((cause) => commandError("The saved login is invalid.", cause)));
30252
+ return {
30253
+ authUrl: credential.authUrl,
30254
+ token: Redacted.make(credential.token)
30255
+ };
30256
+ });
30257
+ const save = Effect.fn("AuthCredentialStore.save")(function* (credential) {
30258
+ const value = {
30259
+ formatVersion: 1,
30260
+ authUrl: credential.authUrl,
30261
+ token: Redacted.value(credential.token)
30262
+ };
30263
+ const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(Credential))(value).pipe(Effect.mapError((cause) => commandError("Could not encode the login.", cause)));
30264
+ yield* fileSystem.makeDirectory(directory, {
30265
+ mode: 448,
30266
+ recursive: true
30267
+ }).pipe(Effect.mapError((cause) => commandError("Could not create the config directory.", cause)));
30268
+ yield* Effect.scoped(Effect.gen(function* () {
30269
+ const temporaryPath = yield* fileSystem.makeTempFileScoped({
30270
+ directory,
30271
+ prefix: ".auth-",
30272
+ suffix: ".json"
30273
+ });
30274
+ yield* fileSystem.writeFileString(temporaryPath, `${encoded}\n`, { mode: 384 });
30275
+ yield* fileSystem.rename(temporaryPath, credentialPath);
30276
+ })).pipe(Effect.mapError((cause) => commandError("Could not save the login.", cause)));
30277
+ });
30278
+ const remove = Effect.fn("AuthCredentialStore.remove")(function* () {
30279
+ if (!(yield* fileSystem.exists(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not check the saved login.", cause))))) return false;
30280
+ yield* fileSystem.remove(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not remove the saved login.", cause)));
30281
+ return true;
30282
+ });
30283
+ return AuthCredentialStore.of({
30284
+ load: load(),
30285
+ remove: remove(),
30286
+ save
30287
+ });
30288
+ }));
30289
+ };
30290
+ const makeClient = (authUrl) => createAuthClient({
30291
+ basePath: "/v1",
30292
+ baseURL: authUrl.href,
30293
+ plugins: [deviceAuthorizationClient(), organizationClient()]
30294
+ });
30295
+ const authorization = (token) => ({ Authorization: `Bearer ${Redacted.value(token)}` });
30296
+ const responseError = (error) => error.error_description ?? error.message ?? "Authentication failed.";
30297
+ const login = Effect.fn("Auth.login")(function* () {
30298
+ const store = yield* AuthCredentialStore;
30299
+ const terminal = yield* Terminal.Terminal;
30300
+ const authUrl = yield* Config.schema(AuthUrl, "IGNOTUM_AUTH_URL").pipe(Config.withDefault(new URL("https://auth.ignotum.cloud")), Effect.mapError((cause) => commandError("The auth URL is invalid.", cause)));
30301
+ const client = makeClient(authUrl);
30302
+ const existing = yield* store.load;
30303
+ if (existing !== void 0 && existing.authUrl.href === authUrl.href) {
30304
+ const result = yield* Effect.tryPromise({
30305
+ try: () => client.getSession({ fetchOptions: { headers: authorization(existing.token) } }),
30306
+ catch: (cause) => commandError("Could not check the saved login.", cause)
30307
+ });
30308
+ if (result.data?.user !== void 0) return {
30309
+ email: result.data.user.email,
30310
+ verificationUrl: void 0
30311
+ };
30312
+ }
30313
+ const started = yield* Effect.tryPromise({
30314
+ try: () => client.device.code({
30315
+ client_id: "ignotum-cli",
30316
+ scope: "openid profile email"
30317
+ }),
30318
+ catch: (cause) => commandError("Could not start login.", cause)
30319
+ });
30320
+ if (started.data === null) return yield* commandError(responseError(started.error));
30321
+ const url = started.data.verification_uri_complete ?? started.data.verification_uri;
30322
+ yield* terminal.display(`Open ${started.data.verification_uri}\nCode: ${started.data.user_code}\n\nWaiting for approval...\n`);
30323
+ yield* Effect.tryPromise({
30324
+ try: async () => (await import("open")).default(url),
30325
+ catch: (cause) => commandError("Could not open the browser.", cause)
30326
+ }).pipe(Effect.ignore);
30327
+ let interval = started.data.interval ?? 5;
30328
+ while (true) {
30329
+ yield* Effect.sleep(`${interval} seconds`);
30330
+ const polled = yield* Effect.tryPromise({
30331
+ try: () => client.device.token({
30332
+ client_id: "ignotum-cli",
30333
+ device_code: started.data.device_code,
30334
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
30335
+ }),
30336
+ catch: (cause) => commandError("Login polling failed.", cause)
30337
+ });
30338
+ if (polled.data?.access_token !== void 0) {
30339
+ const token = Redacted.make(polled.data.access_token);
30340
+ const session = yield* Effect.tryPromise({
30341
+ try: () => client.getSession({ fetchOptions: { headers: authorization(token) } }),
30342
+ catch: (cause) => commandError("Could not read the new session.", cause)
30343
+ });
30344
+ if (session.data?.user === void 0) return yield* commandError("The auth service returned an invalid session.");
30345
+ yield* store.save({
30346
+ authUrl,
30347
+ token
30348
+ });
30349
+ return {
30350
+ email: session.data.user.email,
30351
+ verificationUrl: url
30352
+ };
30353
+ }
30354
+ switch (polled.error?.error) {
30355
+ case "authorization_pending": break;
30356
+ case "slow_down":
30357
+ interval += 5;
30358
+ break;
30359
+ case "access_denied": return yield* commandError("Login was denied.");
30360
+ case "expired_token": return yield* commandError("The login code expired. Run login again.");
30361
+ default: return yield* commandError(polled.error === null ? "Login failed." : responseError(polled.error));
30362
+ }
30363
+ }
30364
+ });
30365
+ const logout = Effect.fn("Auth.logout")(function* () {
30366
+ const store = yield* AuthCredentialStore;
30367
+ const credential = yield* store.load;
30368
+ if (credential === void 0) return false;
30369
+ const client = makeClient(credential.authUrl);
30370
+ const result = yield* Effect.tryPromise({
30371
+ try: () => client.signOut({ fetchOptions: { headers: authorization(credential.token) } }),
30372
+ catch: (cause) => commandError("Could not revoke the session.", cause)
30373
+ });
30374
+ if (result.error !== null) return yield* commandError(responseError(result.error));
30375
+ yield* store.remove;
30376
+ return true;
30377
+ });
30378
+ const status = Effect.fn("Auth.status")(function* () {
30379
+ const credential = yield* (yield* AuthCredentialStore).load;
30380
+ if (credential === void 0) return void 0;
30381
+ const client = makeClient(credential.authUrl);
30382
+ const session = yield* Effect.tryPromise({
30383
+ try: () => client.getSession({ fetchOptions: { headers: authorization(credential.token) } }),
30384
+ catch: (cause) => commandError("Could not check login status.", cause)
30385
+ });
30386
+ if (session.data?.user === void 0) return void 0;
30387
+ const teams = yield* Effect.tryPromise({
30388
+ try: () => client.organization.list({ fetchOptions: { headers: authorization(credential.token) } }),
30389
+ catch: (cause) => commandError("Could not load the team.", cause)
30390
+ });
30391
+ return {
30392
+ email: session.data.user.email,
30393
+ name: session.data.user.name,
30394
+ teamName: teams.data?.[0]?.name
30395
+ };
30396
+ });
29979
30397
  //#endregion
29980
30398
  //#region src/cli/control-client.ts
29981
30399
  const isLoopbackAddress = (hostname) => {
@@ -29984,9 +30402,13 @@ const isLoopbackAddress = (hostname) => {
29984
30402
  };
29985
30403
  const HostedApiUrl = Schema.URL.check(Schema.makeFilter((url) => url.protocol === "https:" || url.protocol === "http:" && isLoopbackAddress(url.hostname), { message: "The API URL must use HTTPS unless it targets a numeric loopback address." }));
29986
30404
  var HostedControlConfiguration = class HostedControlConfiguration extends Context.Service()("ignotum/cli/control-client/HostedControlConfiguration") {
29987
- static layer = Layer.effect(HostedControlConfiguration, Config.all({
29988
- apiUrl: Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(Config.withDefault(new URL("https://api.ignotum.cloud"))),
29989
- token: Config.redacted("IGNOTUM_API_TOKEN")
30405
+ static layer = Layer.effect(HostedControlConfiguration, Effect.gen(function* () {
30406
+ const credential = yield* (yield* AuthCredentialStore).load;
30407
+ if (credential === void 0) return yield* AuthCommandError.make({ message: "Run 'npx ignotum auth login' first." });
30408
+ return {
30409
+ apiUrl: yield* Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(Config.withDefault(new URL("https://api.ignotum.cloud"))),
30410
+ token: credential.token
30411
+ };
29990
30412
  }));
29991
30413
  };
29992
30414
  var ControlApiError = class extends Schema.TaggedError()("ControlApiError", {
@@ -30000,7 +30422,7 @@ var ControlClientUnavailable = class extends Schema.TaggedError()("ControlClient
30000
30422
  }) {};
30001
30423
  var ControlClient = class extends Context.Service()("ignotum/cli/control-client/ControlClient") {
30002
30424
  static get layer() {
30003
- return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer));
30425
+ return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer.pipe(Layer.provide(AuthCredentialStore.layer))));
30004
30426
  }
30005
30427
  };
30006
30428
  const unavailable = (message, cause) => ControlClientUnavailable.make({
@@ -30202,11 +30624,11 @@ const IndexPosition = Schema.Struct({
30202
30624
  key: Schema.String.check(Schema.isPattern(/^(?:[0-9a-f]{2})*$/)).pipe(Schema.brand("ignotum/runtime/EncodedIndexKey"))
30203
30625
  });
30204
30626
  const PaginationPosition = Schema.Union([TablePosition, IndexPosition]);
30205
- const CursorPayload = Schema.Struct({
30627
+ const CursorPayload = initial(Schema.Struct({
30206
30628
  version: Schema.Literal(1),
30207
30629
  query: Schema.String,
30208
30630
  position: PaginationPosition
30209
- });
30631
+ }));
30210
30632
  const CursorPayloadJson = Schema.fromJsonString(CursorPayload);
30211
30633
  const paginationQueryIdentity = (tableId, index, lower, upper, order) => encodeCanonicalJson([
30212
30634
  tableId,
@@ -30563,14 +30985,12 @@ Function$1.dual(2, (previous, next) => previous === void 0 || encodeCanonical(Sc
30563
30985
  indexes: []
30564
30986
  }))
30565
30987
  }));
30566
- //#endregion
30567
- //#region src/cli/app-configuration.ts
30568
- const AppConfiguration = Schema.Struct({
30988
+ const AppConfiguration = initial(Schema.Struct({
30569
30989
  formatVersion: Schema.Literal(1),
30570
30990
  appId: AppId,
30571
30991
  slug: AppSlug,
30572
30992
  apiUrl: Schema.URLFromString
30573
- });
30993
+ }));
30574
30994
  var AppConfigurationInvalid = class extends Schema.TaggedError()("AppConfigurationInvalid", {
30575
30995
  cause: Schema.optional(Schema.Defect()),
30576
30996
  message: Schema.String
@@ -31043,8 +31463,8 @@ var InvalidPublicFile = class extends Schema.TaggedError()("InvalidPublicFile",
31043
31463
  path: Schema.String
31044
31464
  }) {};
31045
31465
  const pathSegmentPattern = /^[A-Za-z0-9._~-]+$/;
31046
- const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
31047
- const asciiAt = (bytes, value, offset = 0) => startsWith(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
31466
+ const startsWith$1 = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
31467
+ const asciiAt = (bytes, value, offset = 0) => startsWith$1(bytes, globalThis.Array.from(value, (character) => character.charCodeAt(0)), offset);
31048
31468
  const hasValidSignature = (extension, bytes) => {
31049
31469
  switch (extension) {
31050
31470
  case ".avif": {
@@ -31054,20 +31474,20 @@ const hasValidSignature = (extension, bytes) => {
31054
31474
  return false;
31055
31475
  }
31056
31476
  case ".gif": return asciiAt(bytes, "GIF87a") || asciiAt(bytes, "GIF89a");
31057
- case ".ico": return startsWith(bytes, [
31477
+ case ".ico": return startsWith$1(bytes, [
31058
31478
  0,
31059
31479
  0,
31060
31480
  1,
31061
31481
  0
31062
31482
  ]);
31063
31483
  case ".jpeg":
31064
- case ".jpg": return startsWith(bytes, [
31484
+ case ".jpg": return startsWith$1(bytes, [
31065
31485
  255,
31066
31486
  216,
31067
31487
  255
31068
31488
  ]);
31069
31489
  case ".pdf": return asciiAt(bytes, "%PDF-");
31070
- case ".png": return startsWith(bytes, [
31490
+ case ".png": return startsWith$1(bytes, [
31071
31491
  137,
31072
31492
  80,
31073
31493
  78,
@@ -31241,10 +31661,13 @@ const hostModuleId = "ignotum:host";
31241
31661
  const encodeJavaScriptString = Schema.encodeSync(Schema.fromJsonString(Schema.String));
31242
31662
  const DiscoveredServerFunction = Schema.Struct({
31243
31663
  address: FunctionAddress,
31664
+ args: Schema.optional(ValueDescriptor),
31665
+ errors: Schema.optional(ValueDescriptor),
31244
31666
  exportName: Schema.String,
31245
31667
  kind: Schema.Literals(["Mutation", "Query"]),
31246
31668
  moduleName: Schema.String,
31247
- modulePath: Schema.String
31669
+ modulePath: Schema.String,
31670
+ returns: Schema.optional(ValueDescriptor)
31248
31671
  });
31249
31672
  const ServerDiscoveryManifest = Schema.Struct({
31250
31673
  functions: Schema.Array(DiscoveredServerFunction),
@@ -31328,12 +31751,50 @@ const discoverModuleFunctions = Effect.fn("Deploy.discoverServerModule")(functio
31328
31751
  });
31329
31752
  continue;
31330
31753
  }
31331
- functions.push({
31754
+ const args = inspected.definition.args === void 0 ? void 0 : {
31755
+ type: "object",
31756
+ fields: descriptorFields(inspected.definition.args)
31757
+ };
31758
+ const returns = inspected.definition.returns === void 0 ? void 0 : getValueDescriptor(inspected.definition.returns);
31759
+ const errors = inspected.definition.errors === void 0 ? void 0 : getValueDescriptor(inspected.definition.errors);
31760
+ if (inspected.definition.returns !== void 0 && returns === void 0 || inspected.definition.errors !== void 0 && errors === void 0) return yield* InvalidServerFunctionExport.make({
31761
+ exportName,
31762
+ message: `${moduleName}.${exportName} uses a validator that was not created by Ignotum values.`,
31763
+ path: modulePath
31764
+ });
31765
+ if (inspected.definition._tag === "Query" && args !== void 0 && descriptorContainsFile(args)) return yield* InvalidServerFunctionExport.make({
31766
+ exportName,
31767
+ message: `${moduleName}.${exportName} cannot accept files in query arguments.`,
31768
+ path: modulePath
31769
+ });
31770
+ if (inspected.definition._tag === "Mutation" && returns !== void 0 && descriptorContainsFile(returns)) return yield* InvalidServerFunctionExport.make({
31771
+ exportName,
31772
+ message: `${moduleName}.${exportName} cannot return files from a mutation.`,
31773
+ path: modulePath
31774
+ });
31775
+ if (errors !== void 0 && descriptorContainsFile(errors)) return yield* InvalidServerFunctionExport.make({
31776
+ exportName,
31777
+ message: `${moduleName}.${exportName} cannot include files in application errors.`,
31778
+ path: modulePath
31779
+ });
31780
+ const discovered = {
31332
31781
  address: FunctionAddress.make(`api.${moduleName}.${exportName}`),
31333
31782
  exportName,
31334
31783
  kind: inspected.definition._tag,
31335
31784
  moduleName,
31336
31785
  modulePath
31786
+ };
31787
+ const withArgs = args === void 0 ? discovered : {
31788
+ ...discovered,
31789
+ args
31790
+ };
31791
+ const withErrors = errors === void 0 ? withArgs : {
31792
+ ...withArgs,
31793
+ errors
31794
+ };
31795
+ functions.push(returns === void 0 ? withErrors : {
31796
+ ...withErrors,
31797
+ returns
31337
31798
  });
31338
31799
  }
31339
31800
  return Array$1.sortWith(functions, (definition) => definition.address, String$1.Order);
@@ -31417,6 +31878,7 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
31417
31878
  appDirectory,
31418
31879
  nodeModulesDirectory(),
31419
31880
  packageRoot,
31881
+ path.join(packageRoot, "node_modules"),
31420
31882
  ...workspaceProbes
31421
31883
  ].map((allowedPath) => `--allow-fs-read=${allowedPath}`);
31422
31884
  const exitCode = yield* spawner.exitCode(ChildProcess.make(process$1.execPath, [
@@ -31523,12 +31985,24 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (a
31523
31985
  if (!(yield* fileSystem.exists(absoluteSourceMap))) return yield* InvalidBuildOutput.make({ message: `${definition.address} did not emit a source map.` });
31524
31986
  const bundlePath = normalizePath(path.join("server", relativeFile));
31525
31987
  const sourceMapPath = normalizePath(path.join("server", relativeSourceMap));
31526
- return {
31988
+ const artifact = {
31527
31989
  address: definition.address,
31528
31990
  kind: definition.kind,
31529
31991
  bundle: yield* artifactReference(bundlePath, yield* fileSystem.readFile(absoluteFile)),
31530
31992
  sourceMap: yield* artifactReference(sourceMapPath, yield* fileSystem.readFile(absoluteSourceMap))
31531
31993
  };
31994
+ const withArgs = definition.args === void 0 ? artifact : {
31995
+ ...artifact,
31996
+ args: definition.args
31997
+ };
31998
+ const withErrors = definition.errors === void 0 ? withArgs : {
31999
+ ...withArgs,
32000
+ errors: definition.errors
32001
+ };
32002
+ return definition.returns === void 0 ? withErrors : {
32003
+ ...withErrors,
32004
+ returns: definition.returns
32005
+ };
31532
32006
  });
31533
32007
  const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, outputDirectory, functionModules) {
31534
32008
  const fileSystem = yield* FileSystem.FileSystem;
@@ -31541,7 +32015,7 @@ const buildServer = Effect.fn("Deploy.buildServer")(function* (appDirectory, out
31541
32015
  yield* fileSystem.writeFile(path.join(outputDirectory, "schema.json"), snapshotBytes);
31542
32016
  const functions = yield* Effect.forEach(discovered.functions, (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition), { concurrency: 4 });
31543
32017
  const manifest = {
31544
- formatVersion: 1,
32018
+ formatVersion: 2,
31545
32019
  schema: yield* artifactReference(schemaSnapshotPath, snapshotBytes),
31546
32020
  functions
31547
32021
  };
@@ -31926,7 +32400,7 @@ const make$2 = (options) => Effect$1.gen(function* () {
31926
32400
  return transformRows ? Effect$1.map(effect, transformRows) : effect;
31927
32401
  },
31928
32402
  executeStream(_sql, _params) {
31929
- return Stream.die("executeStream not implemented");
32403
+ return Stream$1.die("executeStream not implemented");
31930
32404
  },
31931
32405
  backup(destination) {
31932
32406
  return Effect$1.suspend(() => {
@@ -32138,13 +32612,41 @@ const RuntimeQueryResult = Schema.Struct({
32138
32612
  type: Schema.Literal("Query"),
32139
32613
  result: WireResult,
32140
32614
  dependencies: DependencySet,
32141
- observedRevision: AppStateRevision
32615
+ observedRevision: AppStateRevision,
32616
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
32617
+ });
32618
+ const FileReferenceAdded = Schema.Struct({
32619
+ type: Schema.Literal("AddReference"),
32620
+ file: RuntimeFileMetadata,
32621
+ tableId: TableId,
32622
+ documentId: GeneratedId
32142
32623
  });
32624
+ const FileReferenceRemoved = Schema.Struct({
32625
+ type: Schema.Literal("RemoveReference"),
32626
+ fileId: FileId,
32627
+ tableId: TableId,
32628
+ documentId: GeneratedId
32629
+ });
32630
+ const FileReferenceEffect = Schema.Union([FileReferenceAdded, FileReferenceRemoved]);
32631
+ const FileEffectBatch = Schema.Struct({
32632
+ id: InvocationId,
32633
+ committedRevision: AppStateRevision,
32634
+ effects: Schema.Array(FileReferenceEffect)
32635
+ });
32636
+ Schema.Struct({ appId: AppId });
32637
+ Schema.Struct({ batches: Schema.Array(FileEffectBatch) });
32638
+ Schema.Struct({
32639
+ appId: AppId,
32640
+ invocationId: InvocationId,
32641
+ acknowledgedAt: Schema.Int
32642
+ });
32643
+ Schema.Struct({ completed: Schema.Literal(true) });
32143
32644
  const RuntimeMutationSuccess = Schema.Struct({
32144
32645
  type: Schema.Literal("Mutation"),
32145
32646
  result: WireSuccess,
32146
32647
  invalidations: InvalidationSet,
32147
- committedRevision: AppStateRevision
32648
+ committedRevision: AppStateRevision,
32649
+ fileEffects: Schema.optional(FileEffectBatch)
32148
32650
  });
32149
32651
  const RuntimeMutationFailure = Schema.Struct({
32150
32652
  type: Schema.Literal("Mutation"),
@@ -32167,7 +32669,8 @@ const QueryInvocation = Schema.Struct({
32167
32669
  const MutationInvocation = Schema.Struct({
32168
32670
  ...InvocationBase,
32169
32671
  type: Schema.Literal("Mutation"),
32170
- invocationId: InvocationId
32672
+ invocationId: InvocationId,
32673
+ files: Schema.optional(Schema.Array(RuntimeFileOccurrence))
32171
32674
  });
32172
32675
  Schema.Union([QueryInvocation, MutationInvocation]);
32173
32676
  Schema.Struct({
@@ -32256,6 +32759,75 @@ const indexPointInvalidation = (tableId, index, key) => ({
32256
32759
  //#region ../runtime/dist/functions.js
32257
32760
  var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
32258
32761
  //#endregion
32762
+ //#region ../contracts/dist/runtime/transport.js
32763
+ const runtimeInvocationPath = "/v1/invoke";
32764
+ const runtimeRevisionPath = "/v1/revision";
32765
+ const runtimeIndexPreparePath = "/v1/indexes/prepare";
32766
+ const runtimeIndexCommitPath = "/v1/indexes/commit";
32767
+ const runtimeFileEffectsPath = "/v1/files/effects";
32768
+ const runtimeFileEffectsAcknowledgePath = "/v1/files/effects/acknowledge";
32769
+ const ignotumPathPrefix$1 = "/_ignotum";
32770
+ const appSyncPath = `${ignotumPathPrefix$1}/v1/sync`;
32771
+ const fileUploadUrlPrefix = `${ignotumPathPrefix$1}/v1/files/upload/`;
32772
+ const fileGrantUrlPrefix = `${ignotumPathPrefix$1}/v1/files/grant/`;
32773
+ const RuntimeRequestPath = Schema.Literals([
32774
+ runtimeInvocationPath,
32775
+ runtimeRevisionPath,
32776
+ runtimeIndexPreparePath,
32777
+ runtimeIndexCommitPath,
32778
+ runtimeFileEffectsPath,
32779
+ runtimeFileEffectsAcknowledgePath
32780
+ ]);
32781
+ const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
32782
+ const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
32783
+ Schema.Struct({
32784
+ appId: AppId,
32785
+ deploymentId: Schema.optional(DeploymentId),
32786
+ nonce: RuntimeRequestNonce,
32787
+ timestamp: RuntimeRequestTimestamp,
32788
+ signature: RuntimeRequestSignature
32789
+ });
32790
+ Schema.Struct({
32791
+ method: Schema.Literal("POST"),
32792
+ path: RuntimeRequestPath,
32793
+ timestamp: RuntimeRequestTimestamp,
32794
+ nonce: RuntimeRequestNonce,
32795
+ appId: AppId,
32796
+ deploymentId: Schema.optional(DeploymentId),
32797
+ bodySha256: Sha256
32798
+ });
32799
+ Schema.Struct({
32800
+ appId: AppId,
32801
+ deploymentId: DeploymentId
32802
+ });
32803
+ Schema.Struct({ completed: Schema.Literal(true) });
32804
+ const RuntimeErrorCode = Schema.Literals([
32805
+ "InvalidRequest",
32806
+ "InvalidSignature",
32807
+ "RequestExpired",
32808
+ "RequestReplay",
32809
+ "InvocationRejected",
32810
+ "RuntimeUnavailable"
32811
+ ]);
32812
+ Schema.Struct({
32813
+ code: RuntimeErrorCode,
32814
+ message: Schema.String,
32815
+ rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
32816
+ });
32817
+ //#endregion
32818
+ //#region ../shared/dist/http-body.js
32819
+ const joinChunks = (chunks) => {
32820
+ const byteLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
32821
+ const bytes = new Uint8Array(byteLength);
32822
+ let offset = 0;
32823
+ for (const chunk of chunks) {
32824
+ bytes.set(chunk, offset);
32825
+ offset += chunk.byteLength;
32826
+ }
32827
+ return bytes;
32828
+ };
32829
+ const collectStreamBytes = Function$1.dual(3, (stream, maxBytes, onLimitReached) => Stream.mapError(stream, (error) => error).pipe(Stream.limitBytes(maxBytes, () => Stream.fail(onLimitReached())), Stream.runCollect, Effect.map(joinChunks)));
32830
+ //#endregion
32259
32831
  //#region ../runtime/dist/sync.js
32260
32832
  const makeDependencyIndex = () => {
32261
32833
  const dependenciesByQuery = MutableHashMap.empty();
@@ -32326,55 +32898,6 @@ const makeDependencyIndex = () => {
32326
32898
  };
32327
32899
  };
32328
32900
  var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
32329
- //#endregion
32330
- //#region ../contracts/dist/runtime/transport.js
32331
- const runtimeInvocationPath = "/v1/invoke";
32332
- const runtimeRevisionPath = "/v1/revision";
32333
- const runtimeIndexPreparePath = "/v1/indexes/prepare";
32334
- const runtimeIndexCommitPath = "/v1/indexes/commit";
32335
- const appSyncPath = `/_ignotum/v1/sync`;
32336
- const RuntimeRequestPath = Schema.Literals([
32337
- runtimeInvocationPath,
32338
- runtimeRevisionPath,
32339
- runtimeIndexPreparePath,
32340
- runtimeIndexCommitPath
32341
- ]);
32342
- const RuntimeRequestTimestamp = Schema.FiniteFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), Schema.brand("ignotum/runtime/RequestTimestamp"));
32343
- const RuntimeRequestSignature = Sha256.pipe(Schema.brand("ignotum/runtime/RequestSignature"));
32344
- Schema.Struct({
32345
- appId: AppId,
32346
- deploymentId: Schema.optional(DeploymentId),
32347
- nonce: RuntimeRequestNonce,
32348
- timestamp: RuntimeRequestTimestamp,
32349
- signature: RuntimeRequestSignature
32350
- });
32351
- Schema.Struct({
32352
- method: Schema.Literal("POST"),
32353
- path: RuntimeRequestPath,
32354
- timestamp: RuntimeRequestTimestamp,
32355
- nonce: RuntimeRequestNonce,
32356
- appId: AppId,
32357
- deploymentId: Schema.optional(DeploymentId),
32358
- bodySha256: Sha256
32359
- });
32360
- Schema.Struct({
32361
- appId: AppId,
32362
- deploymentId: DeploymentId
32363
- });
32364
- Schema.Struct({ completed: Schema.Literal(true) });
32365
- const RuntimeErrorCode = Schema.Literals([
32366
- "InvalidRequest",
32367
- "InvalidSignature",
32368
- "RequestExpired",
32369
- "RequestReplay",
32370
- "InvocationRejected",
32371
- "RuntimeUnavailable"
32372
- ]);
32373
- Schema.Struct({
32374
- code: RuntimeErrorCode,
32375
- message: Schema.String,
32376
- rejectionCode: Schema.optional(RuntimeInvocationRejectionCode)
32377
- });
32378
32901
  const syncPath = appSyncPath;
32379
32902
  const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
32380
32903
  //#endregion
@@ -32450,9 +32973,21 @@ const addDevelopmentIndexes = Effect.gen(function* () {
32450
32973
  ON indexEntries (indexId, key)
32451
32974
  `;
32452
32975
  });
32976
+ const addApplicationFiles = Effect.gen(function* () {
32977
+ yield* (yield* Client.SqlClient)`
32978
+ CREATE TABLE applicationFiles (
32979
+ fileId TEXT PRIMARY KEY NOT NULL,
32980
+ name TEXT NOT NULL,
32981
+ format TEXT NOT NULL CHECK (format IN ('jpeg', 'png', 'webp', 'avif', 'gif')),
32982
+ size INTEGER NOT NULL CHECK (size >= 0),
32983
+ ready INTEGER NOT NULL DEFAULT 0 CHECK (ready IN (0, 1))
32984
+ ) STRICT
32985
+ `;
32986
+ });
32453
32987
  const developmentMigrationLoader = Migrator.fromRecord({
32454
32988
  "0001_initial_development_schema": initialDevelopmentSchema,
32455
- "0002_application_indexes": addDevelopmentIndexes
32989
+ "0002_application_indexes": addDevelopmentIndexes,
32990
+ "0003_application_files": addApplicationFiles
32456
32991
  });
32457
32992
  var DevelopmentDatabase = class DevelopmentDatabase extends Context.Service()("ignotum/dev-runtime/migrations/DevelopmentDatabase") {
32458
32993
  static layer = Layer.effect(DevelopmentDatabase, Migrator.make({})({
@@ -32846,68 +33381,65 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
32846
33381
  return id;
32847
33382
  });
32848
33383
  const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id, invalidations) {
32849
- const table = yield* resolveTable(schema, tableName);
33384
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32850
33385
  const documentId = GeneratedId.make(id);
32851
33386
  const stored = yield* findStored({
32852
33387
  id,
32853
33388
  tableId: table.id
32854
- });
32855
- const keys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
32856
- yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`;
33389
+ }).pipe(Effect.orDie);
33390
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33391
+ const keys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33392
+ yield* sql`DELETE FROM documents WHERE id = ${id} AND tableId = ${table.id}`.pipe(Effect.orDie);
32857
33393
  invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...keys.map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32858
33394
  });
32859
33395
  const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value, invalidations) {
32860
- const table = yield* resolveTable(schema, tableName);
33396
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32861
33397
  const documentId = GeneratedId.make(id);
32862
33398
  const stored = yield* findStored({
32863
33399
  id,
32864
33400
  tableId: table.id
32865
- });
32866
- const oldKeys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
32867
- let newKeys = oldKeys.slice(0, 0);
32868
- if (Option.isSome(stored)) {
32869
- const current = yield* decodeFields(table.definition, tableName, stored.value, "patch");
32870
- const fields = yield* encodeFields(table.definition, tableName, {
32871
- ...current,
32872
- ...value
32873
- }, "patch", id);
32874
- const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
32875
- yield* sql`
32876
- UPDATE documents
32877
- SET fields = ${fields}, updatedAt = ${updatedAt}
32878
- WHERE id = ${id} AND tableId = ${table.id}
32879
- `;
32880
- newKeys = yield* writeIndexEntries(table, {
32881
- ...stored.value,
32882
- fields,
32883
- updatedAt
32884
- });
32885
- }
33401
+ }).pipe(Effect.orDie);
33402
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33403
+ const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33404
+ const current = yield* decodeFields(table.definition, tableName, stored.value, "patch").pipe(Effect.orDie);
33405
+ const fields = yield* encodeFields(table.definition, tableName, {
33406
+ ...current,
33407
+ ...value
33408
+ }, "patch", id).pipe(Effect.orDie);
33409
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
33410
+ yield* sql`
33411
+ UPDATE documents
33412
+ SET fields = ${fields}, updatedAt = ${updatedAt}
33413
+ WHERE id = ${id} AND tableId = ${table.id}
33414
+ `.pipe(Effect.orDie);
33415
+ const newKeys = yield* writeIndexEntries(table, {
33416
+ ...stored.value,
33417
+ fields,
33418
+ updatedAt
33419
+ }).pipe(Effect.orDie);
32886
33420
  invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32887
33421
  });
32888
33422
  const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value, invalidations) {
32889
- const table = yield* resolveTable(schema, tableName);
33423
+ const table = yield* resolveTable(schema, tableName).pipe(Effect.orDie);
32890
33424
  const documentId = GeneratedId.make(id);
32891
33425
  const stored = yield* findStored({
32892
33426
  id,
32893
33427
  tableId: table.id
32894
- });
32895
- const oldKeys = Option.isSome(stored) ? yield* indexKeys(table, stored.value) : [];
32896
- let newKeys = oldKeys.slice(0, 0);
32897
- if (Option.isSome(stored)) {
32898
- const fields = yield* encodeFields(table.definition, tableName, value, "replace", id);
32899
- const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
32900
- yield* sql`
32901
- UPDATE documents
32902
- SET fields = ${fields}, updatedAt = ${updatedAt}
32903
- WHERE id = ${id} AND tableId = ${table.id}
32904
- `;
32905
- newKeys = yield* writeIndexEntries(table, {
32906
- ...stored.value,
32907
- fields,
32908
- updatedAt
32909
- });
32910
- }
33428
+ }).pipe(Effect.orDie);
33429
+ if (Option.isNone(stored)) return yield* Effect.fail(documentNotFound(tableName, id));
33430
+ const oldKeys = yield* indexKeys(table, stored.value).pipe(Effect.orDie);
33431
+ const fields = yield* encodeFields(table.definition, tableName, value, "replace", id).pipe(Effect.orDie);
33432
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
33433
+ yield* sql`
33434
+ UPDATE documents
33435
+ SET fields = ${fields}, updatedAt = ${updatedAt}
33436
+ WHERE id = ${id} AND tableId = ${table.id}
33437
+ `.pipe(Effect.orDie);
33438
+ const newKeys = yield* writeIndexEntries(table, {
33439
+ ...stored.value,
33440
+ fields,
33441
+ updatedAt
33442
+ }).pipe(Effect.orDie);
32911
33443
  invalidations.record(tableDependency(table.id), documentDependency(table.id, documentId), ...[...oldKeys, ...newKeys].map(({ identity, key }) => indexPointInvalidation(table.id, identity, key)));
32912
33444
  });
32913
33445
  const makeReader = (schema, dependencies) => {
@@ -33021,10 +33553,10 @@ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-r
33021
33553
  const reader = makeReader(schema);
33022
33554
  return Object.freeze({
33023
33555
  ...reader,
33024
- delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations).pipe(Effect.orDie)),
33556
+ delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id, invalidations)),
33025
33557
  insert: (tableName, value) => resultFromEffect(insert(schema, tableName, value, invalidations).pipe(Effect.orDie)),
33026
- patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations).pipe(Effect.orDie)),
33027
- replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations).pipe(Effect.orDie))
33558
+ patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value, invalidations)),
33559
+ replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations))
33028
33560
  });
33029
33561
  };
33030
33562
  const trackedQueryTransaction = (schema, use) => sql.withTransaction(Effect.gen(function* () {
@@ -33135,12 +33667,19 @@ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum
33135
33667
  onFailure: (error) => encodeFailure(resolved.definition, error),
33136
33668
  onSuccess: (value) => encodeSuccess(resolved.definition, value)
33137
33669
  }));
33138
- return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) => ({
33139
- type: "Query",
33140
- result: value,
33141
- dependencies,
33142
- observedRevision
33143
- }))) : mutationSemaphore.withPermits(1)(database.trackedMutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.map(({ committedRevision, invalidations, value }) => ({
33670
+ return yield* (kind === "Query" ? database.trackedQueryTransaction(resolved.schema, invoke).pipe(Effect.map(({ dependencies, observedRevision, value }) => {
33671
+ const files = value.type === "Success" && value.value !== void 0 ? encodedFileOccurrencesOf(value.value) : [];
33672
+ const result = {
33673
+ type: "Query",
33674
+ result: value,
33675
+ dependencies,
33676
+ observedRevision
33677
+ };
33678
+ return files.length === 0 ? result : {
33679
+ ...result,
33680
+ files
33681
+ };
33682
+ })) : mutationSemaphore.withPermits(1)(database.trackedMutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.map(({ committedRevision, invalidations, value }) => ({
33144
33683
  type: "Mutation",
33145
33684
  result: value,
33146
33685
  invalidations,
@@ -33189,6 +33728,255 @@ const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function*
33189
33728
  }) });
33190
33729
  }));
33191
33730
  //#endregion
33731
+ //#region ../contracts/dist/schema/file-content.js
33732
+ const ascii = (bytes) => {
33733
+ let value = "";
33734
+ for (let offset = 0; offset < bytes.length; offset += 16384) value += String.fromCharCode(...bytes.subarray(offset, offset + 16384));
33735
+ return value;
33736
+ };
33737
+ const startsWith = (bytes, signature) => signature.every((byte, index) => bytes[index] === byte);
33738
+ const endsWith = (bytes, signature) => signature.every((byte, index) => bytes[bytes.length - signature.length + index] === byte);
33739
+ const validJpeg = (bytes) => bytes.length >= 4 && startsWith(bytes, [
33740
+ 255,
33741
+ 216,
33742
+ 255
33743
+ ]) && endsWith(bytes, [255, 217]);
33744
+ const validPng = (bytes) => bytes.length >= 20 && startsWith(bytes, [
33745
+ 137,
33746
+ 80,
33747
+ 78,
33748
+ 71,
33749
+ 13,
33750
+ 10,
33751
+ 26,
33752
+ 10
33753
+ ]) && ascii(bytes.subarray(bytes.length - 12, bytes.length - 8)) === "IEND";
33754
+ const validGif = (bytes) => bytes.length >= 14 && (ascii(bytes.subarray(0, 6)) === "GIF87a" || ascii(bytes.subarray(0, 6)) === "GIF89a") && bytes.at(-1) === 59;
33755
+ const validWebp = (bytes) => {
33756
+ if (bytes.length < 12 || ascii(bytes.subarray(0, 4)) !== "RIFF" || ascii(bytes.subarray(8, 12)) !== "WEBP") return false;
33757
+ return ((bytes[4] ?? 0) | (bytes[5] ?? 0) << 8 | (bytes[6] ?? 0) << 16 | (bytes[7] ?? 0) << 24) + 8 === bytes.length;
33758
+ };
33759
+ const validAvif = (bytes) => {
33760
+ if (bytes.length < 16 || ascii(bytes.subarray(4, 8)) !== "ftyp") return false;
33761
+ const boxLength = (bytes[0] ?? 0) << 24 | (bytes[1] ?? 0) << 16 | (bytes[2] ?? 0) << 8 | (bytes[3] ?? 0);
33762
+ if (boxLength < 16 || boxLength > bytes.length) return false;
33763
+ for (let offset = 8; offset + 4 <= boxLength; offset += 4) {
33764
+ const brand = ascii(bytes.subarray(offset, offset + 4));
33765
+ if (brand === "avif" || brand === "avis") return true;
33766
+ }
33767
+ return false;
33768
+ };
33769
+ const validateApplicationFile = (bytes, format, declaredSize) => {
33770
+ if (bytes.byteLength !== declaredSize) return {
33771
+ valid: false,
33772
+ message: "The uploaded byte length does not match its preparation."
33773
+ };
33774
+ if (bytes.byteLength === 0 || bytes.byteLength > fileLimits.fileBytes) return {
33775
+ valid: false,
33776
+ message: `Application files must be between 1 and ${fileLimits.fileBytes} bytes.`
33777
+ };
33778
+ return (format === "jpeg" ? validJpeg(bytes) : format === "png" ? validPng(bytes) : format === "gif" ? validGif(bytes) : format === "webp" ? validWebp(bytes) : validAvif(bytes)) ? { valid: true } : {
33779
+ valid: false,
33780
+ message: `The upload is not a valid ${format} image.`
33781
+ };
33782
+ };
33783
+ //#endregion
33784
+ //#region src/dev-runtime/files.ts
33785
+ Schema.Struct({
33786
+ fileId: FileId,
33787
+ name: Schema.String,
33788
+ format: RuntimeFileOccurrence.fields.file.fields.format,
33789
+ size: Schema.Natural,
33790
+ ready: Schema.Literals([0, 1])
33791
+ });
33792
+ Schema.Struct({ fields: Schema.String });
33793
+ const JsonObjectString = Schema.fromJsonString(Schema.JsonObject);
33794
+ const LocalApplicationFiles = Context.Reference("ignotum/dev-runtime/files/LocalApplicationFiles", { defaultValue: () => ({
33795
+ admit: (_invocationId, _functionAddress, args) => Effect.succeed(encodedFileOccurrencesOf(args)),
33796
+ grants: (_subscriptionId, files) => Effect.succeed(Array.from(new Set(files.map(({ file }) => file.id)), (id) => ({
33797
+ id,
33798
+ url: ""
33799
+ }))),
33800
+ prepare: (_invocationId, _functionAddress, _args, fileIds) => Effect.succeed(fileIds.map((id) => ({ id }))),
33801
+ upload: () => Effect.die("Local application file storage is unavailable."),
33802
+ releaseGrants: () => Effect.void,
33803
+ readGrant: () => Effect.succeed(Option.none()),
33804
+ reconcile: Effect.void,
33805
+ releasePreparation: () => Effect.void
33806
+ }) });
33807
+ const sameFile = (left, right) => left.id === right.id && left.format === right.format && left.name === right.name && left.size === right.size;
33808
+ const validFilename = (name) => {
33809
+ const bytes = new TextEncoder().encode(name).byteLength;
33810
+ const hasControlCharacter = Array.from(name).some((character) => {
33811
+ const codePoint = character.codePointAt(0) ?? 0;
33812
+ return codePoint <= 31 || codePoint === 127;
33813
+ });
33814
+ return bytes > 0 && bytes <= fileLimits.filenameBytes && !hasControlCharacter && !name.includes("/") && !name.includes("\\");
33815
+ };
33816
+ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicationFiles, Effect.gen(function* () {
33817
+ const ids = yield* IdGenerator;
33818
+ const fileSystem = yield* FileSystem.FileSystem;
33819
+ const path = yield* Path$1.Path;
33820
+ const sql = yield* SqlClient.SqlClient;
33821
+ const directory = path.join(appDirectory, ".ignotum", "files");
33822
+ yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie);
33823
+ const preparations = /* @__PURE__ */ new Map();
33824
+ const uploadFiles = /* @__PURE__ */ new Map();
33825
+ const grants = /* @__PURE__ */ new Map();
33826
+ const objectPath = (fileId) => path.join(directory, fileId);
33827
+ const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, fileIds) {
33828
+ if (fileIds.length === 0) throw new Error("A file preparation must contain a file ID.");
33829
+ const selected = new Set(fileIds);
33830
+ if (selected.size !== fileIds.length) throw new Error("A file preparation contains a duplicate file ID.");
33831
+ const files = encodedFileOccurrencesOf(args).filter((occurrence) => selected.has(occurrence.file.id));
33832
+ if (new Set(files.map((occurrence) => occurrence.file.id)).size !== selected.size) throw new Error("Prepared files do not match the mutation arguments.");
33833
+ const existing = preparations.get(invocationId);
33834
+ if (existing !== void 0) {
33835
+ if (existing.function !== functionAddress || encodeCanonicalJson(existing.args) !== encodeCanonicalJson(args) || encodeCanonicalJson(existing.files) !== encodeCanonicalJson(files)) throw new Error("The invocation ID already belongs to another file preparation.");
33836
+ return fileIds.map((id) => {
33837
+ const token = existing.uploads.get(id);
33838
+ return token === void 0 ? { id } : {
33839
+ id,
33840
+ url: `${fileUploadUrlPrefix}${token}`
33841
+ };
33842
+ });
33843
+ }
33844
+ const distinct = /* @__PURE__ */ new Map();
33845
+ for (const occurrence of files) {
33846
+ if (!validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
33847
+ const prior = distinct.get(occurrence.file.id);
33848
+ if (prior !== void 0 && !sameFile(prior, occurrence.file)) throw new Error("One file ID has conflicting metadata.");
33849
+ distinct.set(occurrence.file.id, occurrence.file);
33850
+ }
33851
+ if (distinct.size > fileLimits.filesPerMutation) throw new Error("Too many files.");
33852
+ if ([...distinct.values()].reduce((total, file) => total + file.size, 0) > fileLimits.mutationBytes) throw new Error("File uploads are too large.");
33853
+ const staged = yield* sql`
33854
+ SELECT COALESCE(SUM(size), 0) AS bytes FROM applicationFiles WHERE ready = 0
33855
+ `;
33856
+ let additionalStagedBytes = 0;
33857
+ for (const file of distinct.values()) {
33858
+ const stored = (yield* sql`
33859
+ SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
33860
+ `)[0];
33861
+ if (stored === void 0) additionalStagedBytes += file.size;
33862
+ else if (!sameFile({
33863
+ id: stored.fileId,
33864
+ name: stored.name,
33865
+ format: stored.format,
33866
+ size: stored.size
33867
+ }, file)) throw new Error(`File '${file.id}' has conflicting metadata.`);
33868
+ }
33869
+ if ((staged[0]?.bytes ?? 0) + additionalStagedBytes > fileLimits.stagedBytesPerApp) throw new Error("The app has too many staged file bytes.");
33870
+ const uploads = /* @__PURE__ */ new Map();
33871
+ for (const file of distinct.values()) {
33872
+ const stored = (yield* sql`
33873
+ SELECT fileId, name, format, size, ready FROM applicationFiles WHERE fileId = ${file.id}
33874
+ `)[0];
33875
+ if (stored !== void 0 && stored.ready === 1) continue;
33876
+ const token = yield* ids.generate(FileUploadToken);
33877
+ uploads.set(file.id, token);
33878
+ uploadFiles.set(token, file);
33879
+ yield* sql`
33880
+ INSERT INTO applicationFiles (fileId, name, format, size, ready)
33881
+ VALUES (${file.id}, ${file.name}, ${file.format}, ${file.size}, 0)
33882
+ ON CONFLICT(fileId) DO UPDATE SET
33883
+ name = excluded.name, format = excluded.format, size = excluded.size
33884
+ `;
33885
+ }
33886
+ preparations.set(invocationId, {
33887
+ function: functionAddress,
33888
+ args,
33889
+ files,
33890
+ uploads
33891
+ });
33892
+ return fileIds.map((id) => {
33893
+ const token = uploads.get(id);
33894
+ return token === void 0 ? { id } : {
33895
+ id,
33896
+ url: `${fileUploadUrlPrefix}${token}`
33897
+ };
33898
+ });
33899
+ });
33900
+ const admit = Effect.fn("LocalApplicationFiles.admit")(function* (invocationId, functionAddress, args) {
33901
+ const preparation = preparations.get(invocationId);
33902
+ if (preparation !== void 0 && (preparation.function !== functionAddress || encodeCanonicalJson(preparation.args) !== encodeCanonicalJson(args))) throw new Error("The mutation does not match its file preparation.");
33903
+ for (const occurrence of preparation?.files ?? []) if ((yield* sql`
33904
+ SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
33905
+ `)[0]?.ready !== 1) throw new Error("A prepared file has not finished uploading.");
33906
+ const occurrences = encodedFileOccurrencesOf(args);
33907
+ for (const occurrence of occurrences) if ((yield* sql`
33908
+ SELECT ready FROM applicationFiles WHERE fileId = ${occurrence.file.id}
33909
+ `)[0]?.ready !== 1) throw new Error(`File '${occurrence.file.id}' is unavailable.`);
33910
+ return occurrences;
33911
+ });
33912
+ const upload = Effect.fn("LocalApplicationFiles.upload")(function* (token, bytes) {
33913
+ const file = uploadFiles.get(token);
33914
+ if (file === void 0) throw new Error("The local file upload ticket is invalid.");
33915
+ const validation = validateApplicationFile(bytes, file.format, file.size);
33916
+ if (!validation.valid) throw new Error(validation.message);
33917
+ yield* fileSystem.writeFile(objectPath(file.id), bytes).pipe(Effect.orDie);
33918
+ yield* sql`UPDATE applicationFiles SET ready = 1 WHERE fileId = ${file.id}`;
33919
+ uploadFiles.delete(token);
33920
+ return "Stored";
33921
+ });
33922
+ const grantFiles = Effect.fn("LocalApplicationFiles.grants")(function* (subscriptionId, occurrences) {
33923
+ for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
33924
+ const tokens = /* @__PURE__ */ new Map();
33925
+ for (const occurrence of occurrences) {
33926
+ if (tokens.has(occurrence.file.id)) continue;
33927
+ const token = yield* ids.generate(FileGrantToken);
33928
+ tokens.set(occurrence.file.id, token);
33929
+ grants.set(token, {
33930
+ subscriptionId,
33931
+ file: occurrence.file
33932
+ });
33933
+ }
33934
+ return Array.from(tokens, ([id, token]) => ({
33935
+ id,
33936
+ url: `${fileGrantUrlPrefix}${token}`
33937
+ }));
33938
+ });
33939
+ const reconcile = Effect.gen(function* () {
33940
+ const rows = yield* sql`
33941
+ SELECT fields FROM documents
33942
+ `;
33943
+ const referenced = /* @__PURE__ */ new Set();
33944
+ for (const row of rows) {
33945
+ const fields = yield* Schema.decodeEffect(JsonObjectString)(row.fields).pipe(Effect.orDie);
33946
+ for (const occurrence of encodedFileOccurrencesOf(fields)) referenced.add(occurrence.file.id);
33947
+ }
33948
+ const stored = yield* sql`
33949
+ SELECT fileId, name, format, size, ready FROM applicationFiles
33950
+ `;
33951
+ for (const file of stored) {
33952
+ if (referenced.has(file.fileId)) continue;
33953
+ yield* fileSystem.remove(objectPath(file.fileId), { force: true }).pipe(Effect.orDie);
33954
+ yield* sql`DELETE FROM applicationFiles WHERE fileId = ${file.fileId}`;
33955
+ }
33956
+ });
33957
+ return LocalApplicationFiles.of({
33958
+ admit: (...args) => admit(...args).pipe(Effect.orDie),
33959
+ grants: grantFiles,
33960
+ readGrant: (token) => {
33961
+ const grant = grants.get(token);
33962
+ return grant === void 0 ? Effect.succeed(Option.none()) : fileSystem.readFile(objectPath(grant.file.id)).pipe(Effect.orDie, Effect.map((bytes) => Option.some({
33963
+ file: grant.file,
33964
+ bytes
33965
+ })));
33966
+ },
33967
+ reconcile: reconcile.pipe(Effect.orDie),
33968
+ releaseGrants: (subscriptionId) => Effect.sync(() => {
33969
+ for (const [token, grant] of grants) if (grant.subscriptionId === subscriptionId) grants.delete(token);
33970
+ }),
33971
+ releasePreparation: (invocationId) => Effect.suspend(() => {
33972
+ if (!preparations.delete(invocationId)) return Effect.void;
33973
+ return reconcile.pipe(Effect.orDie);
33974
+ }),
33975
+ prepare: (...args) => prepare(...args).pipe(Effect.orDie),
33976
+ upload: (...args) => upload(...args).pipe(Effect.orDie)
33977
+ });
33978
+ }));
33979
+ //#endregion
33192
33980
  //#region src/dev-runtime/sync.ts
33193
33981
  const localSyncIdentity = {
33194
33982
  appId: AppId.make("app_000000000000000000000001"),
@@ -33249,14 +34037,14 @@ const operationForSubscription = (subscriptionId) => ({
33249
34037
  type: "Subscription",
33250
34038
  id: subscriptionId
33251
34039
  });
33252
- const protocolError = (code, message, operation) => {
34040
+ const syncError = (code, message, operation) => {
33253
34041
  if (operation === void 0) return {
33254
- type: "ProtocolError",
34042
+ type: "Error",
33255
34043
  code,
33256
34044
  message
33257
34045
  };
33258
34046
  return {
33259
- type: "ProtocolError",
34047
+ type: "Error",
33260
34048
  code,
33261
34049
  message,
33262
34050
  operation
@@ -33266,6 +34054,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33266
34054
  const runtime = yield* FunctionRuntime;
33267
34055
  const invalidation = yield* QueryInvalidation;
33268
34056
  const mutationReplay = yield* MutationReplay;
34057
+ const files = yield* LocalApplicationFiles;
33269
34058
  const subscriptions = yield* Ref.make(HashMap.empty());
33270
34059
  const dependencyIndex = makeDependencyIndex();
33271
34060
  const invocationFibers = yield* FiberSet.make();
@@ -33277,7 +34066,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33277
34066
  });
33278
34067
  const sendResolutionError = (operation, error, deliver = send) => {
33279
34068
  const code = error._tag;
33280
- return deliver(protocolError(code, error.message, operation));
34069
+ return deliver(syncError(code, error.message, operation));
33281
34070
  };
33282
34071
  const isActive = (subscriptionId, subscription) => Ref.get(subscriptions).pipe(Effect.map((current) => Option.getOrUndefined(HashMap.get(current, subscriptionId)) === subscription));
33283
34072
  const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (subscriptionId, subscription, message) {
@@ -33304,11 +34093,16 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33304
34093
  if (!(yield* isActive(subscriptionId, subscription)) || subscription.refreshGeneration !== refreshGeneration) return;
33305
34094
  dependencyIndex.record(subscriptionId, result.dependencies);
33306
34095
  if ((yield* invalidation.latestRevision) > result.observedRevision) continue;
33307
- yield* deliver({
34096
+ const granted = yield* files.grants(subscriptionId, result.files ?? []);
34097
+ const snapshot = {
33308
34098
  type: "Snapshot",
33309
34099
  id: subscriptionId,
33310
34100
  result: result.result,
33311
34101
  revision: result.observedRevision
34102
+ };
34103
+ yield* deliver(granted.length === 0 ? snapshot : {
34104
+ ...snapshot,
34105
+ files: granted
33312
34106
  });
33313
34107
  return;
33314
34108
  }
@@ -33336,9 +34130,13 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33336
34130
  yield* PubSub.take(invalidations).pipe(Effect.flatMap((event) => event.type === "All" ? refreshAll() : refreshAffected(event.invalidations)), Effect.forever, Effect.forkScoped);
33337
34131
  const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
33338
34132
  const operation = operationForSubscription(message.id);
34133
+ if (encodedFileOccurrencesOf(message.args).length > 0) {
34134
+ yield* send(syncError("InvalidArguments", "Queries cannot take files as arguments.", operation));
34135
+ return;
34136
+ }
33339
34137
  const current = yield* Ref.get(subscriptions);
33340
34138
  if (HashMap.has(current, message.id)) {
33341
- yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
34139
+ yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
33342
34140
  return;
33343
34141
  }
33344
34142
  const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
@@ -33362,13 +34160,20 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33362
34160
  type: "Invocation",
33363
34161
  id: message.id
33364
34162
  };
34163
+ if (!(yield* files.admit(message.id, message.function, message.args).pipe(Effect.as(true), Effect.catchCause((cause) => send(syncError("InvalidArguments", `The mutation files could not be admitted: ${String(cause)}`, operation)).pipe(Effect.as(false)))))) {
34164
+ yield* files.releasePreparation(message.id);
34165
+ return;
34166
+ }
33365
34167
  const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
33366
34168
  FunctionUnavailable: (error) => sendResolutionError(operation, error),
33367
34169
  InvalidArguments: (error) => sendResolutionError(operation, error),
33368
34170
  UnknownFunction: (error) => sendResolutionError(operation, error),
33369
34171
  WrongFunctionKind: (error) => sendResolutionError(operation, error)
33370
34172
  }));
33371
- if (prepared === void 0) return;
34173
+ if (prepared === void 0) {
34174
+ yield* files.releasePreparation(message.id);
34175
+ return;
34176
+ }
33372
34177
  const input = canonicalInvocationInput(localSyncIdentity.deploymentId, "Mutation", message.function, message.args);
33373
34178
  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) => {
33374
34179
  if (!("committedRevision" in result)) return send({
@@ -33386,14 +34191,26 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33386
34191
  invalidations: result.invalidations,
33387
34192
  committedRevision: result.committedRevision
33388
34193
  })));
33389
- }), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
34194
+ }), Effect.catchTags({ InvocationIdConflict: () => send(syncError("InvocationIdConflict", `Invocation ${message.id} was already used with different inputs.`, operation)) }), Effect.ensuring(files.releasePreparation(message.id)), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
33390
34195
  cause,
33391
34196
  function: message.function,
33392
34197
  requestId: message.id
33393
34198
  }))), FiberSet.run(invocationFibers), Effect.asVoid);
33394
34199
  });
34200
+ const handlePrepare = Effect.fn("SyncServer.handlePrepare")(function* (message) {
34201
+ const operation = {
34202
+ type: "Invocation",
34203
+ id: message.id
34204
+ };
34205
+ yield* files.prepare(message.id, message.function, message.args, message.files).pipe(Effect.flatMap((uploads) => send({
34206
+ type: "Preparation",
34207
+ id: message.id,
34208
+ kind: message.kind,
34209
+ uploads
34210
+ })), Effect.catchCause((cause) => send(syncError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34211
+ });
33395
34212
  const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
33396
- const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
34213
+ const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(syncError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
33397
34214
  if (message === void 0) return;
33398
34215
  switch (message.type) {
33399
34216
  case "Subscribe":
@@ -33402,6 +34219,10 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33402
34219
  case "Unsubscribe":
33403
34220
  dependencyIndex.remove(message.id);
33404
34221
  yield* Ref.update(subscriptions, HashMap.remove(message.id));
34222
+ yield* files.releaseGrants(message.id);
34223
+ return;
34224
+ case "Prepare":
34225
+ yield* handlePrepare(message);
33405
34226
  return;
33406
34227
  case "Invoke":
33407
34228
  yield* handleInvoke(message);
@@ -33411,7 +34232,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33411
34232
  yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)), { onOpen: send({
33412
34233
  type: "Handshake",
33413
34234
  ...localSyncIdentity
33414
- }).pipe(Effect.orDie) });
34235
+ }).pipe(Effect.orDie) }).pipe(Effect.ensuring(Ref.get(subscriptions).pipe(Effect.flatMap((active) => Effect.forEach(HashMap.keys(active), (subscriptionId) => files.releaseGrants(subscriptionId))), Effect.asVoid)));
33415
34236
  });
33416
34237
  var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
33417
34238
  const makeHandlersLayer = (server, appDirectory, databasePath) => {
@@ -33419,7 +34240,7 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
33419
34240
  const persistenceLayer = Layer.merge(LocalDatabase.layer, mutationReplayLayer).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.merge(idGeneratorLayer, sqliteLayer)));
33420
34241
  const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(persistenceLayer), Layer.provide(idGeneratorLayer));
33421
34242
  const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, appDirectory).pipe(Layer.provide(layer$1)), executorLayer)));
33422
- const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1);
34243
+ const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, persistenceLayer, layer$1, localApplicationFilesLayer(appDirectory).pipe(Layer.provideMerge(DevelopmentDatabase.layer), Layer.provide(Layer.mergeAll(idGeneratorLayer, sqliteLayer, layer$1))));
33423
34244
  return Layer.effect(SyncHandlers, Effect.gen(function* () {
33424
34245
  const invalidation = yield* QueryInvalidation;
33425
34246
  const fileSystem = yield* FileSystem.FileSystem;
@@ -33435,6 +34256,30 @@ const makeHandlersLayer = (server, appDirectory, databasePath) => {
33435
34256
  const httpApp = Effect.gen(function* () {
33436
34257
  const request = yield* HttpServerRequest$1.HttpServerRequest;
33437
34258
  const pathname = new URL(request.url, "http://ignotum.local").pathname;
34259
+ if (pathname.startsWith(fileUploadUrlPrefix)) {
34260
+ if (request.method !== "PUT") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
34261
+ const token = Schema.decodeOption(FileUploadToken)(pathname.slice(fileUploadUrlPrefix.length));
34262
+ if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
34263
+ const bytes = yield* collectStreamBytes(request.stream, fileLimits.fileBytes, () => /* @__PURE__ */ new Error("The local application file is too large.")).pipe(Effect.orDie);
34264
+ return yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.upload(token.value, bytes)), Effect.match({
34265
+ onFailure: (error) => HttpServerResponse.text(String(error), { status: 422 }),
34266
+ onSuccess: () => HttpServerResponse.text("Stored", { status: 201 })
34267
+ }));
34268
+ }
34269
+ if (pathname.startsWith(fileGrantUrlPrefix)) {
34270
+ if (request.method !== "GET" && request.method !== "HEAD") return HttpServerResponse.text("Method Not Allowed", { status: 405 });
34271
+ const token = Schema.decodeOption(FileGrantToken)(pathname.slice(fileGrantUrlPrefix.length));
34272
+ if (Option.isNone(token)) return HttpServerResponse.text("Not Found", { status: 404 });
34273
+ const granted = yield* LocalApplicationFiles.pipe(Effect.flatMap((storage) => storage.readGrant(token.value)));
34274
+ if (Option.isNone(granted)) return HttpServerResponse.text("Not Found", { status: 404 });
34275
+ const headers = {
34276
+ "cache-control": "private, no-store",
34277
+ "content-length": String(granted.value.bytes.byteLength),
34278
+ "content-type": fileMimeTypes[granted.value.file.format],
34279
+ "x-content-type-options": "nosniff"
34280
+ };
34281
+ return request.method === "HEAD" ? HttpServerResponse.empty({ headers }) : HttpServerResponse.uint8Array(granted.value.bytes, { headers });
34282
+ }
33438
34283
  return yield* HttpServerResponse.json(pathname === syncPath ? {
33439
34284
  code: "UpgradeRequired",
33440
34285
  message: `Connect to ${syncPath} with WebSocket.`
@@ -33614,30 +34459,58 @@ const dev$1 = Effect.fn("Dev.run")(function* (options) {
33614
34459
  return yield* Effect.never;
33615
34460
  }));
33616
34461
  });
34462
+ //#endregion
34463
+ //#region ../../docs/agent/AGENTS.md?raw
34464
+ 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";
34465
+ //#endregion
34466
+ //#region ../../docs/agent/skills/ignotum/SKILL.md?raw
34467
+ 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| 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 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";
34468
+ //#endregion
34469
+ //#region src/cli/agent-files.ts
34470
+ const references = [
34471
+ ["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"],
34472
+ ["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"],
34473
+ ["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"],
34474
+ ["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"],
34475
+ ["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"],
34476
+ ["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.\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. To return to earlier behavior, deploy that source again as a new\ndeployment.\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"],
34477
+ ["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, and\npublic assets. Ignotum activates these parts together after the complete deployment passes its\nchecks.\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"],
34478
+ ["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"],
34479
+ ["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"],
34480
+ ["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`,\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, environment, or process access. Use the APIs\nprovided through the function context.\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"],
34481
+ ["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. See [Functions](functions.md) for function module naming rules.\n"],
34482
+ ["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"],
34483
+ ["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"],
34484
+ ["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"],
34485
+ ["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"],
34486
+ ["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\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"],
34487
+ ["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.\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"],
34488
+ ["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"],
34489
+ ["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"],
34490
+ ["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"],
34491
+ ["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 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. 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"],
34492
+ ["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"],
34493
+ ["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"],
34494
+ ["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"],
34495
+ ["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"],
34496
+ ["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"],
34497
+ ["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"],
34498
+ ["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\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\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"],
34499
+ ["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"],
34500
+ ["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"]
34501
+ ];
34502
+ const withoutFrontmatter = (content) => content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
33617
34503
  const agentAppFiles = [
33618
34504
  {
33619
- content: "# AGENTS.md\n\nThis is an Ignotum app. Before changing it, read `.agents/skills/ignotum/SKILL.md`.\n\nThe skill explains how to inspect the app, which bundled reference to read, and how to check\nyour changes.\n",
34505
+ content: AGENTS_default,
33620
34506
  path: "AGENTS.md"
33621
34507
  },
33622
34508
  {
33623
- content: "---\nname: ignotum\ndescription: Build and modify an Ignotum app. Use when working on its schema, server functions, generated API, client UI, or local development workflow.\n---\n\n# Working on an Ignotum app\n\nAn Ignotum app defines a database schema, server functions, and a JSX client. Ignotum supplies the\ndatabase runtime, generated bindings, realtime query updates, Vite dev server, JSX runtime, and\nTailwind setup.\n\nThe current release supports local development. Do not assume that authentication, files, actions,\nworkflows, scheduled jobs, or production deployment APIs exist.\n\n## Start with the app\n\nInspect `server/schema.ts`, the function modules in `server`, the files in `client`, `package.json`,\nand `tsconfig.json` before changing code.\n\nThe required `client/index.tsx` default-exports `app({ title, component })` from `ignotum/client`.\nKeep the title as a non-empty quoted string. An optional `client/icon.svg` is discovered as the app\nfavicon. Do not add another metadata file or import Ignotum's Tailwind stylesheet.\n\nTreat `_generated` as compiler output. Read it when you need to understand an inferred type, but do\nnot edit it. After code changes, regenerate the bindings and run the typechecker:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nFor normal Ignotum app work, do not add an HTML file, Vite configuration, Tailwind configuration,\nAPI routes, or direct database setup.\n\n## Read the relevant reference\n\nThe bundled references are the Ignotum user guides. Read only the guides needed for the task:\n\n- For app creation and installation, read [getting started](references/getting-started.md) or\n [manual setup](references/manual-setup.md).\n- For tables, fields, indexes, IDs, and generated document types, read\n [schema syntax](references/schema.md).\n- For validators and their TypeScript types, read [values](references/values.md).\n- For queries, mutations, Results, and application errors, read\n [server functions](references/server-functions.md).\n- For document reads, indexes, ranges, ordering, pagination, and result methods, read\n [database reads](references/reading-data.md).\n- For inserts, patches, replacements, deletes, and transaction behavior, read\n [database writes](references/writing-data.md).\n- For hooks, query state, paginated lists, mutations, JSX, and Tailwind, read\n [client](references/client.md).\n- For development commands, code generation, flags, and local data, read\n [dev server](references/dev-server.md).\n- For the complete guide list, read the [documentation index](references/index.md).\n\n## Rules that cross guide boundaries\n\n- Define tables in `server/schema.ts`.\n- Put queries and mutations in TypeScript files directly inside `server`.\n- Import server builders from `@/_generated/server.js` and client references from\n `@/_generated/api.js`. Keep the `.js` suffix.\n- Put code used by both the client and server in `shared` and import it through `@/shared`.\n- Never edit `_generated`.\n- Use JSX and hooks from `ignotum/client`, not React or Preact packages directly.\n- Keep the app inside Ignotum's current model unless the user explicitly asks to move beyond it.\n",
34509
+ content: SKILL_default,
33624
34510
  path: ".agents/skills/ignotum/SKILL.md"
33625
34511
  },
33626
- ...[
33627
- ["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. The required\n`client/index.tsx` file defines the browser title and root component:\n\n```tsx\nimport { app, Query, Result, useMutation, usePaginatedQuery, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n\nfunction App() {\n return <main>My App</main>;\n}\n\nexport default app({\n title: \"My App\",\n component: App,\n});\n```\n\nThe title must be a non-empty quoted string in the `app(...)` definition so Ignotum can include it\nin the first HTML response.\n\n## Tailwind CSS\n\nIgnotum loads Tailwind CSS automatically. App code does not need to import a framework stylesheet.\n\nStyle JSX with Tailwind utility classes. Use the JSX `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\nThe app does not need a Tailwind configuration file. Custom CSS files are ordinary client modules:\ngive them any filename and import them from app code when needed.\n\n## Public files\n\nPut files that need their own URL in a top-level `public` directory next to `client` and `server`.\nThe directory structure becomes the URL structure. For example, `public/documents/manual.pdf` is\navailable at `/documents/manual.pdf` after deployment.\n\nPublic files are limited to AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF. Ignotum checks that a file's\ncontents match its extension and rejects symbolic links, executable or code formats, and paths\nunder the reserved `_ignotum` name. Use `client/icon.svg` for the app favicon; arbitrary SVG files\nare not accepted in `public`. Put the directory at the app root—`client/public` is rejected.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\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\nIgnotum keeps an active query up to date. A successful mutation refreshes affected subscribed\nqueries in every open client. Queries that did not read the changed documents or tables do not run\nagain.\n\n## Load a paginated query\n\n`usePaginatedQuery` calls a query with a `pagination: values.pagination()` argument and a\n`values.page(...)` return value. Pass only the query's other arguments. The hook supplies the\ncursor and uses 20 items per page by default:\n\n```tsx\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\nPass `{ pageSize: number }` as the third argument to choose another size. The hook keeps earlier\nitems visible while it loads the next page. Changing the query arguments starts again from the\nfirst page. `Result.match` does not need another matcher because `LoadingMore` still has usable\nitems.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\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 client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Operation-scoped protocol failures reject mutation\npromises or reach the nearest query UI error boundary. Ignotum retries transient connection failures,\nkeeps the latest query state, and replays pending mutations after the next connection handshake.\n\nHosted apps limit connections, subscriptions, active unique queries, and mutation traffic. See\n[Limits](limits.md) for the current values and retry retention.\n"],
33628
- ["deploy.md", "# Deploy\n\nSet your API token in your shell:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nDo not put the token in the app directory or commit it to source control.\n\nOn the first deploy, pass the app slug:\n\n```sh\nnpx ignotum deploy --app my-app\n```\n\nIf the slug belongs to one of your apps, Ignotum links it. Otherwise, Ignotum creates the app.\nWithout `--app`, the first deploy asks for the slug interactively.\n\nIgnotum stores the app ID, slug, and API URL in `.ignotum/app.json`. It never stores the access\ntoken there. Later deploys reuse the saved app, so the command has no app flag:\n\n```sh\nnpx ignotum deploy\n```\n\nThere is no separate build command. `deploy` generates the current bindings and builds both parts\nof the application, uploads the artifact, and makes the new deployment active.\n\nThe client output contains the SPA shell, generated assets, and validated files from the top-level\n`public` directory. The server output contains one bundle for every exported query or mutation.\nIgnotum writes both under `.ignotum/build` only after the complete build succeeds. If a rebuild\nfails, the previous successful output stays in place.\n\nUploads stream from disk. If an upload is interrupted after it begins, Ignotum leaves the active\ndeployment unchanged and prints the incomplete deployment ID. A rejected upload reports the\nAPI's reason. A successful deploy prints the app URL and deployment ID.\n\nThe CLI uses `https://api.ignotum.cloud` by default. Set `IGNOTUM_API_URL` only when targeting a\ndifferent Ignotum API, such as a local or development deployment.\n\nIgnotum rejects deployments that exceed the hosted file or artifact size limits. It also removes\nunfinished uploads and older inactive deployments after their retention periods. See\n[Limits](limits.md) for the current values.\n"],
33629
- ["dev-server.md", "# Dev server\n\nRun the dev server from the app root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/index.tsx\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, reads the `app(...)` definition from\n`client/index.tsx`, loads Tailwind automatically, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, Tailwind configuration, or framework stylesheet.\nAdd an optional `client/icon.svg` and Ignotum uses it as the favicon automatically. With no icon\nfile, the HTML contains no favicon link.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
33630
- ["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`. When it is ready, follow the [deploy guide](deploy.md) to create or link\nthe hosted app and publish it.\n"],
33631
- ["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development and hosted deployment.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, and application errors.\n- [Read from the database](reading-data.md) covers documents, indexes, ranges, ordering, and result methods.\n- [Write to the database](writing-data.md) covers inserts, patches, replacements, deletes, and mutation behavior.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n- [Deploy](deploy.md) creates or links an app, then uploads and activates a deployment.\n- [Limits](limits.md) lists hosted limits for functions, data, realtime updates, and deployments.\n"],
33632
- ["limits.md", "# 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 connection, traffic, time, result, and storage-quota failures as\n`ResourceLimitExceeded`. A database read or write that crosses a document or collection limit can\nappear as a temporary function failure. A deployment that exceeds a limit fails before activation,\nso the current deployment stays active.\n\n## Server 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\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 one query execution when app data changes while it runs. That 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\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"],
33633
- ["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the app generator. It uses pnpm to install dependencies.\n\nCreate the app directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n index.tsx\n icon.svg\n public/\n manual.pdf\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"1.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\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\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.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\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) + counterIncrement;\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\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/index.tsx`:\n\n```tsx\nimport { app, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nfunction App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n\nexport default app({\n title: \"Counter\",\n component: App,\n});\n```\n\nIgnotum loads Tailwind automatically. You do not need an HTML file, Vite configuration, Tailwind\nconfiguration, or framework stylesheet. Custom CSS files are ordinary client modules and can use\nany filename when imported from app code.\n\nThe `client/icon.svg` file is optional. When present, Ignotum discovers it automatically and uses it\nas the favicon. When absent, the app has no favicon link.\n\nThe top-level `public` directory is optional. Its AVIF, GIF, ICO, JPEG, PNG, WebP, and PDF files keep\ntheir relative paths as public URLs. Do not put this directory inside `client`.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md),\n[database reads](reading-data.md), [database writes](writing-data.md), and the\n[client guide](client.md) to continue building the app.\n"],
33634
- ["reading-data.md", "# Read from the database\n\nQuery and mutation handlers can read documents through `ctx.db`. Every read is explicit: a table\nquery reads that table, while an indexed query follows an index declared in the schema. Ignotum\ndoes not silently load a table and filter it to make an unindexed operation look efficient.\n\n## Read one document\n\nUse `find` when a missing document is an ordinary result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with `DocumentNotFound`, which contains the table and ID. You can expose it,\ncatch it, or map it to one of your own application errors.\n\n## Read a table\n\n`collect` returns every document in the query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nThis is the right API when the table is intentionally small or the function genuinely needs every\ndocument. It is a full table read, not an indexed lookup. Hosted collection limits still apply.\n\nQueries use ascending `createdAt` order by default, with `id` breaking ties. Reverse the order with\n`order(\"desc\")`:\n\n```ts\nconst newestFirst = yield * ctx.db.query(\"todos\").order(\"desc\").take(20);\n```\n\n## Read through an index\n\nDeclare indexes in the schema, then select one by name:\n\n```ts\n// server/schema.ts\ntodos: table({\n channel: values.string(),\n priority: values.integer(),\n text: values.string(),\n}).index(\"by_channel_priority\", [\"channel\", \"priority\"]);\n```\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\nRange fields must follow the index order. Use equality on any leading fields, then optionally add a\nlower bound, an upper bound, or both on the next field:\n\n| Method | Matches |\n| ------------------- | -------------------------------- |\n| `eq(field, value)` | Equal to `value` |\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\nFor a two-sided range, chain the bounds on the same field:\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 scans that index in its declared order:\n\n```ts\nconst byPriority = yield * ctx.db.query(\"todos\").index(\"by_priority\").collect();\n```\n\nEvery index orders by its declared fields, then `createdAt`, then `id`. The two system fields are\navailable as range fields after all declared fields have been matched with `eq`.\n\n## Choose how many documents to return\n\nAll table and index queries support these terminal methods:\n\n| Method | Result |\n| ------------------- | ---------------------------------------------------------------------- |\n| `collect()` | Every matching document |\n| `take(number)` | At most `number` matching documents |\n| `first()` | The first matching document, or `undefined` |\n| `unique()` | The only matching document, `undefined`, or a failure if several match |\n| `paginate(options)` | One page and a cursor for the next page |\n\n`unique()` checks a query result; it does not add a uniqueness constraint to the index. Use\n`first()` when several matches are valid and only the first one matters.\n\nUse `take` or `first` whenever the function only needs a bounded result. Use `collect` when reading\nthe complete matching set is intentional.\n\n## Read pages\n\nAdd a `pagination` argument and return a page validator when a client should load a long ordered\nlist in parts:\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\nA page contains `items` and `nextCursor`. A `null` cursor means there are no more matching\ndocuments. Pass a non-null cursor back through `pagination` to continue the same table, index,\nrange, and order. Cursors are opaque and must not be parsed or changed.\n\nPage sizes are integers from 1 through 1,000. Prefer an index for paginated filters so the database\ncan read the requested range directly.\n\n## Reads in mutations\n\nMutation handlers receive the same read API and observe their earlier writes in that mutation.\nThis makes read-modify-write logic straightforward:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\nyield * ctx.db.patch(\"todos\", todo.id, { completed: !todo.completed });\n```\n\nSee [Write to the database](writing-data.md) for the write methods and transaction behavior.\n"],
33635
- ["schema.md", "# Schema syntax\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## Indexes\n\nChain `index` after a table to declare an ordered index. The first argument is its name; the second\nis the field order:\n\n```ts\nmessages: table({\n channel: values.string(),\n authorId: values.id(\"users\"),\n text: values.string(),\n})\n .index(\"by_channel\", [\"channel\"])\n .index(\"by_channel_author\", [\"channel\", \"authorId\"]);\n```\n\nIndex fields must be required booleans, dates, IDs, integers, numbers, strings, or literals. Field\norder matters: `by_channel_author` can efficiently select one channel, or one channel and author,\nbut it is not an author-only index.\n\nEvery index uses `createdAt` and `id` as its final ordering fields. Read\n[Read from the database](reading-data.md) for selecting indexes, building ranges, ordering results,\nand choosing a result method.\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"],
33636
- ["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nconst Todo = values.doc(\"todos\");\n\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database access\n\nQuery handlers receive the read API. Mutation handlers receive the same reads plus writes, and all\nof a mutation's writes commit together.\n\n- [Read from the database](reading-data.md) covers `find`, `get`, table and index queries, ranges,\n ordering, `collect`, `take`, `first`, and `unique`.\n- [Write to the database](writing-data.md) covers `insert`, `patch`, `replace`, `delete`, and\n mutation behavior.\n\nHosted apps limit document size, collection reads, function runtime, and stored app data. See\n[Limits](limits.md) for the current values.\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\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"],
33637
- ["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\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.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\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 works from the result of the previous method. Added fields can be picked, omitted, or\noverridden immediately. An omitted name can be 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\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"],
33638
- ["writing-data.md", "# Write to the database\n\nMutation handlers can insert, patch, replace, and delete documents through `ctx.db`. Query handlers\nonly receive the read API.\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 table field. Omit optional fields when they have no value. Ignotum supplies\n`id`, `createdAt`, and `updatedAt`; never pass those system fields yourself.\n\n## Patch\n\n`patch` changes only the supplied fields:\n\n```ts\nyield * ctx.db.patch(\"todos\", args.id, { completed: true });\n```\n\nAll other fields keep their current values. Patching a missing document has no effect.\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. The document keeps its `id` and `createdAt`, while\n`updatedAt` advances. Replacing a missing document has no effect.\n\n## Delete\n\n`delete` removes a document by ID:\n\n```ts\nyield * ctx.db.delete(\"todos\", args.id);\n```\n\nDeleting a missing document has no effect.\n\n## Mutation behavior\n\nA mutation sees its earlier writes, including through indexed reads. Ignotum commits all of the\nmutation's writes together after the handler succeeds. If the handler fails with an application\nerror or encounters an internal failure, none of its writes are committed.\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\nRead [Read from the database](reading-data.md) for `find`, `get`, table queries, indexed ranges,\nordering, and result methods.\n"]
33639
- ].map(([name, content]) => ({
33640
- content,
34512
+ ...references.map(([name, content]) => ({
34513
+ content: withoutFrontmatter(content),
33641
34514
  path: `.agents/skills/ignotum/references/${name}`
33642
34515
  }))
33643
34516
  ];
@@ -33828,6 +34701,18 @@ Open <http://127.0.0.1:3210>.
33828
34701
  The app generator creates \`_generated\`. The dev server checks those files before it starts
33829
34702
  and updates them when the schema or server functions change.
33830
34703
 
34704
+ ## Deploy
34705
+
34706
+ Sign in, then choose a unique slug on the first deployment:
34707
+
34708
+ \`\`\`sh
34709
+ npx ignotum auth login
34710
+ npx ignotum deploy --app my-app
34711
+ \`\`\`
34712
+
34713
+ The command prints the hosted URL and records the app link in \`.ignotum/app.json\`. Later
34714
+ deployments use \`npx ignotum deploy\` without the \`--app\` flag.
34715
+
33831
34716
  ## App files
33832
34717
 
33833
34718
  - \`server/schema.ts\` defines the database tables.
@@ -33837,6 +34722,7 @@ and updates them when the schema or server functions change.
33837
34722
  - Ignotum loads Tailwind CSS automatically. Custom CSS files are ordinary client modules.
33838
34723
  - \`shared/utils.ts\` contains code shared across the app.
33839
34724
  - \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
34725
+ - \`.agents/skills/ignotum\` contains the app skill and references used by coding agents.
33840
34726
 
33841
34727
  Run the typechecker after a change:
33842
34728
 
@@ -33845,6 +34731,9 @@ npx ignotum codegen
33845
34731
  npx tsc --noEmit
33846
34732
  \`\`\`
33847
34733
 
34734
+ The [Ignotum documentation](https://docs.ignotum.cloud) covers schema values, indexes, queries,
34735
+ mutations, client hooks, deployment, guarantees, and hosted limits.
34736
+
33848
34737
  ## Claude Code
33849
34738
 
33850
34739
  If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
@@ -33968,6 +34857,40 @@ const DevPort = Schema.Int.check(Schema.isBetween({
33968
34857
  minimum: 1,
33969
34858
  maximum: 65535
33970
34859
  }));
34860
+ const authLogin = Command.make("login", {}, Effect.fn("auth login")(function* () {
34861
+ const terminal = yield* Terminal.Terminal;
34862
+ const result = yield* login().pipe(Effect.mapError((error) => CliError.UserError.make({
34863
+ cause: error,
34864
+ userMessage: error.message
34865
+ })));
34866
+ yield* terminal.display(`Signed in as ${result.email}.\n`);
34867
+ })).pipe(Command.withDescription("Sign in to Ignotum with GitHub."));
34868
+ const authLogout = Command.make("logout", {}, Effect.fn("auth logout")(function* () {
34869
+ const terminal = yield* Terminal.Terminal;
34870
+ const removed = yield* logout().pipe(Effect.mapError((error) => CliError.UserError.make({
34871
+ cause: error,
34872
+ userMessage: error.message
34873
+ })));
34874
+ yield* terminal.display(removed ? "Signed out.\n" : "Not signed in.\n");
34875
+ })).pipe(Command.withDescription("Revoke and remove the saved login."));
34876
+ const authStatus = Command.make("status", {}, Effect.fn("auth status")(function* () {
34877
+ const terminal = yield* Terminal.Terminal;
34878
+ const current = yield* status().pipe(Effect.mapError((error) => CliError.UserError.make({
34879
+ cause: error,
34880
+ userMessage: error.message
34881
+ })));
34882
+ if (current === void 0) {
34883
+ yield* terminal.display("Not signed in.\n");
34884
+ return;
34885
+ }
34886
+ const team = current.teamName === void 0 ? "" : `\nTeam: ${current.teamName}`;
34887
+ yield* terminal.display(`Signed in as ${current.name} (${current.email}).${team}\n`);
34888
+ })).pipe(Command.withDescription("Show the current Ignotum login."));
34889
+ const auth = Command.make("auth").pipe(Command.withDescription("Manage your Ignotum login."), Command.withSubcommands([
34890
+ authLogin,
34891
+ authLogout,
34892
+ authStatus
34893
+ ]));
33971
34894
  const codegen = Command.make("codegen", {}, Effect.fn("codegen")(function* () {
33972
34895
  const path = yield* Path$1.Path;
33973
34896
  const terminal = yield* Terminal.Terminal;
@@ -34054,10 +34977,11 @@ const ignotum = Command.make("ignotum").pipe(Command.withSubcommands([
34054
34977
  install,
34055
34978
  codegen,
34056
34979
  dev,
34057
- deploy
34980
+ deploy,
34981
+ auth
34058
34982
  ]));
34059
34983
  const main = () => {
34060
- ignotum.pipe(Command.run({ version }), Effect.provide(Layer.mergeAll(layer$1, layerUndici, IdGenerator.layer)), runMain);
34984
+ ignotum.pipe(Command.run({ version }), Effect.provide(Layer.mergeAll(layer$1, layerUndici, IdGenerator.layer, AuthCredentialStore.layer.pipe(Layer.provide(layer$1)))), runMain);
34061
34985
  };
34062
34986
  //#endregion
34063
34987
  //#region src/cli/bin.ts