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.
- package/README.md +89 -101
- package/dist/cli/bin.mjs +1119 -195
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DtX8qPrq.js → api-DzcR7spt.js} +36 -13
- package/dist/runtime/api-DzcR7spt.js.map +1 -0
- package/dist/runtime/{api-Dp4J-xt-.d.ts → api-jCl8Hzry.d.ts} +7 -9
- package/dist/runtime/client.d.ts +12 -5
- package/dist/runtime/client.js +285 -60
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-t6BOEGw9-C1rwYIlx.js → descriptor-C5VA9qRl-BKbdenuU.js} +38 -7
- package/dist/runtime/descriptor-C5VA9qRl-BKbdenuU.js.map +1 -0
- package/dist/runtime/file-C1abuMgd.js +173 -0
- package/dist/runtime/file-C1abuMgd.js.map +1 -0
- package/dist/runtime/{id-Btwac71X-DhnKYsjY.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +51 -5
- package/dist/runtime/{index-B5KSOjGN.d.ts → index-D6VLTbDB.d.ts} +40 -21
- package/dist/runtime/internal/api.d.ts +2 -2
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +8 -7
- package/dist/runtime/internal/host.js +21 -10
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-B1BzNkh8-BUSTbeSg.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +8 -4
- package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
- package/dist/runtime/{schema-D9RmboaS.js → schema-B6PK_ZwV.js} +17 -3
- package/dist/runtime/schema-B6PK_ZwV.js.map +1 -0
- package/dist/runtime/server.d.ts +3 -3
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/sync-bMQq9tXx.d.ts +8 -0
- package/package.json +7 -5
- package/src/cli/agent-files.ts +52 -13
- package/src/cli/app-configuration.ts +4 -1
- package/src/cli/auth-client.ts +286 -0
- package/src/cli/build/server.ts +83 -6
- package/src/cli/command.ts +65 -2
- package/src/cli/control-client.ts +19 -6
- package/src/cli/new-app.ts +16 -0
- package/src/client/errors.ts +7 -10
- package/src/client/files.ts +153 -0
- package/src/client/hooks.ts +2 -15
- package/src/client/index.ts +7 -0
- package/src/client/sync.ts +158 -30
- package/src/dev-runtime/database.ts +69 -57
- package/src/dev-runtime/files.ts +337 -0
- package/src/dev-runtime/functions.ts +14 -6
- package/src/dev-runtime/migrations.ts +14 -0
- package/src/dev-runtime/sync.ts +134 -22
- package/src/internal/api.ts +16 -1
- package/src/server/index.ts +8 -1
- package/dist/runtime/api-DtX8qPrq.js.map +0 -1
- package/dist/runtime/descriptor-t6BOEGw9-C1rwYIlx.js.map +0 -1
- package/dist/runtime/id-D570vudg.js +0 -26
- package/dist/runtime/id-D570vudg.js.map +0 -1
- package/dist/runtime/pagination-BKPko9Hm.d.ts +0 -1
- 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
|
|
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
|
|
29540
|
-
type: Schema.Literal("
|
|
29691
|
+
const SyncError = Schema.Struct({
|
|
29692
|
+
type: Schema.Literal("Error"),
|
|
29541
29693
|
operation: Schema.optional(Operation),
|
|
29542
|
-
code:
|
|
29694
|
+
code: ErrorCode,
|
|
29543
29695
|
message: Schema.String
|
|
29544
29696
|
});
|
|
29545
|
-
const
|
|
29546
|
-
type: Schema.Literal("
|
|
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
|
-
|
|
29556
|
-
|
|
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-
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
29988
|
-
|
|
29989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
32856
|
-
|
|
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
|
-
|
|
32867
|
-
|
|
32868
|
-
|
|
32869
|
-
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
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
|
-
|
|
32896
|
-
|
|
32897
|
-
|
|
32898
|
-
|
|
32899
|
-
|
|
32900
|
-
|
|
32901
|
-
|
|
32902
|
-
|
|
32903
|
-
|
|
32904
|
-
|
|
32905
|
-
|
|
32906
|
-
|
|
32907
|
-
|
|
32908
|
-
|
|
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)
|
|
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)
|
|
33027
|
-
replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value, invalidations)
|
|
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
|
|
33140
|
-
result
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
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
|
|
34040
|
+
const syncError = (code, message, operation) => {
|
|
33253
34041
|
if (operation === void 0) return {
|
|
33254
|
-
type: "
|
|
34042
|
+
type: "Error",
|
|
33255
34043
|
code,
|
|
33256
34044
|
message
|
|
33257
34045
|
};
|
|
33258
34046
|
return {
|
|
33259
|
-
type: "
|
|
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(
|
|
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*
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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:
|
|
34505
|
+
content: AGENTS_default,
|
|
33620
34506
|
path: "AGENTS.md"
|
|
33621
34507
|
},
|
|
33622
34508
|
{
|
|
33623
|
-
content:
|
|
34509
|
+
content: SKILL_default,
|
|
33624
34510
|
path: ".agents/skills/ignotum/SKILL.md"
|
|
33625
34511
|
},
|
|
33626
|
-
...[
|
|
33627
|
-
|
|
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
|