ignotum 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +6 -2
  2. package/dist/cli/bin.mjs +294 -64
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-BTeMI5tx.js → api-DzcR7spt.js} +22 -23
  5. package/dist/runtime/api-DzcR7spt.js.map +1 -0
  6. package/dist/runtime/{api-D9lV-5av.d.ts → api-jCl8Hzry.d.ts} +3 -3
  7. package/dist/runtime/client.d.ts +5 -5
  8. package/dist/runtime/client.js +57 -55
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{descriptor-C5VA9qRl-C338iC6l.js → descriptor-C5VA9qRl-BKbdenuU.js} +2 -2
  11. package/dist/runtime/{descriptor-C5VA9qRl-C338iC6l.js.map → descriptor-C5VA9qRl-BKbdenuU.js.map} +1 -1
  12. package/dist/runtime/{file-BXf63ulU.js → file-C1abuMgd.js} +8 -1
  13. package/dist/runtime/file-C1abuMgd.js.map +1 -0
  14. package/dist/runtime/{id-Btwac71X-DGj0DQuu.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +2 -2
  15. package/dist/runtime/{index-CF04_Dps.d.ts → index-D6VLTbDB.d.ts} +4 -4
  16. package/dist/runtime/internal/api.d.ts +1 -1
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +4 -4
  19. package/dist/runtime/internal/host.js +3 -3
  20. package/dist/runtime/internal/server.d.ts +1 -1
  21. package/dist/runtime/internal/server.js +1 -1
  22. package/dist/runtime/internal/types.d.ts +1 -1
  23. package/dist/runtime/internal/types.js +1 -1
  24. package/dist/runtime/{pagination-DrOowBve-Bnipd34u.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +3 -3
  25. package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
  26. package/dist/runtime/{schema-DJfwfq87.js → schema-B6PK_ZwV.js} +3 -3
  27. package/dist/runtime/{schema-DJfwfq87.js.map → schema-B6PK_ZwV.js.map} +1 -1
  28. package/dist/runtime/server.d.ts +2 -2
  29. package/dist/runtime/server.js +2 -2
  30. package/dist/runtime/{sync-mIXn9eKv.d.ts → sync-bMQq9tXx.d.ts} +2 -2
  31. package/package.json +4 -2
  32. package/src/cli/auth-client.ts +286 -0
  33. package/src/cli/command.ts +65 -2
  34. package/src/cli/control-client.ts +19 -6
  35. package/src/cli/new-app.ts +2 -1
  36. package/src/client/errors.ts +7 -10
  37. package/src/client/files.ts +36 -51
  38. package/src/client/sync.ts +35 -23
  39. package/src/dev-runtime/files.ts +30 -31
  40. package/src/dev-runtime/sync.ts +19 -25
  41. package/dist/runtime/api-BTeMI5tx.js.map +0 -1
  42. package/dist/runtime/file-BXf63ulU.js.map +0 -1
  43. package/dist/runtime/pagination-BNFhAjns.d.ts +0 -1
package/dist/cli/bin.mjs CHANGED
@@ -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, SchemaGetter, Semaphore, Stream, 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";
@@ -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");
@@ -29596,18 +29606,18 @@ const Invoke = Schema.Struct({
29596
29606
  function: FunctionAddress,
29597
29607
  args: Schema.Json
29598
29608
  });
29599
- const PrepareMutation = Schema.Struct({
29600
- type: Schema.Literal("PrepareMutation"),
29609
+ const Prepare = Schema.Struct({
29610
+ type: Schema.Literal("Prepare"),
29601
29611
  id: InvocationId,
29602
29612
  kind: Schema.Literal("Mutation"),
29603
29613
  function: FunctionAddress,
29604
29614
  args: Schema.Json,
29605
- files: Schema.Array(RuntimeFileOccurrence)
29615
+ files: Schema.Array(FileId)
29606
29616
  });
29607
29617
  const ClientMessage = Schema.Union([
29608
29618
  Subscribe,
29609
29619
  Unsubscribe,
29610
- PrepareMutation,
29620
+ Prepare,
29611
29621
  Invoke
29612
29622
  ]);
29613
29623
  const SubscriptionOperation = Schema.Struct({
@@ -29644,17 +29654,16 @@ const Snapshot = Schema.Struct({
29644
29654
  result: WireResult,
29645
29655
  revision: AppStateRevision,
29646
29656
  files: Schema.optional(Schema.Array(Schema.Struct({
29647
- path: RuntimeFileOccurrence.fields.path,
29648
- file: RuntimeFileMetadata,
29657
+ id: FileId,
29649
29658
  url: Schema.String
29650
29659
  })))
29651
29660
  });
29652
- const MutationPrepared = Schema.Struct({
29653
- type: Schema.Literal("MutationPrepared"),
29661
+ const Preparation = Schema.Struct({
29662
+ type: Schema.Literal("Preparation"),
29654
29663
  id: InvocationId,
29664
+ kind: Schema.Literal("Mutation"),
29655
29665
  uploads: Schema.Array(Schema.Struct({
29656
- path: RuntimeFileOccurrence.fields.path,
29657
- file: RuntimeFileMetadata,
29666
+ id: FileId,
29658
29667
  url: Schema.optional(Schema.String)
29659
29668
  }))
29660
29669
  });
@@ -29669,7 +29678,7 @@ const SyncResultFailure = Schema.Struct({
29669
29678
  id: InvocationId,
29670
29679
  result: WireFailure
29671
29680
  });
29672
- const ProtocolErrorCode = Schema.Literals([
29681
+ const ErrorCode = Schema.Literals([
29673
29682
  "DuplicateOperationId",
29674
29683
  "FunctionUnavailable",
29675
29684
  "InvalidArguments",
@@ -29679,25 +29688,25 @@ const ProtocolErrorCode = Schema.Literals([
29679
29688
  "UnknownFunction",
29680
29689
  "WrongFunctionKind"
29681
29690
  ]);
29682
- const ProtocolError = Schema.Struct({
29683
- type: Schema.Literal("ProtocolError"),
29691
+ const SyncError = Schema.Struct({
29692
+ type: Schema.Literal("Error"),
29684
29693
  operation: Schema.optional(Operation),
29685
- code: ProtocolErrorCode,
29694
+ code: ErrorCode,
29686
29695
  message: Schema.String
29687
29696
  });
29688
- const DeploymentChanged = Schema.Struct({
29689
- type: Schema.Literal("DeploymentChanged"),
29697
+ const Deployment = Schema.Struct({
29698
+ type: Schema.Literal("Deployment"),
29690
29699
  deploymentId: DeploymentId,
29691
29700
  generation: DeploymentGeneration
29692
29701
  });
29693
29702
  const ServerMessage = Schema.Union([
29694
29703
  SyncHandshake,
29695
29704
  Snapshot,
29696
- MutationPrepared,
29705
+ Preparation,
29697
29706
  SyncResultSuccess,
29698
29707
  SyncResultFailure,
29699
- ProtocolError,
29700
- DeploymentChanged
29708
+ SyncError,
29709
+ Deployment
29701
29710
  ]);
29702
29711
  const ClientMessageJson = Schema.fromJsonString(ClientMessage);
29703
29712
  const ServerMessageJson = Schema.fromJsonString(ServerMessage);
@@ -30080,7 +30089,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
30080
30089
  };
30081
30090
  //#endregion
30082
30091
  //#region package.json
30083
- var version = "0.0.8";
30092
+ var version = "0.0.9";
30084
30093
  //#endregion
30085
30094
  //#region src/cli/codegen.ts
30086
30095
  const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
@@ -30208,6 +30217,183 @@ const generate = Effect.fn("Codegen.generate")(function* (appDirectory) {
30208
30217
  written
30209
30218
  };
30210
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
+ });
30211
30397
  //#endregion
30212
30398
  //#region src/cli/control-client.ts
30213
30399
  const isLoopbackAddress = (hostname) => {
@@ -30216,9 +30402,13 @@ const isLoopbackAddress = (hostname) => {
30216
30402
  };
30217
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." }));
30218
30404
  var HostedControlConfiguration = class HostedControlConfiguration extends Context.Service()("ignotum/cli/control-client/HostedControlConfiguration") {
30219
- static layer = Layer.effect(HostedControlConfiguration, Config.all({
30220
- apiUrl: Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(Config.withDefault(new URL("https://api.ignotum.cloud"))),
30221
- token: Config.redacted("IGNOTUM_API_TOKEN")
30405
+ static layer = Layer.effect(HostedControlConfiguration, Effect.gen(function* () {
30406
+ const credential = yield* (yield* AuthCredentialStore).load;
30407
+ if (credential === void 0) return yield* AuthCommandError.make({ message: "Run 'npx ignotum auth login' first." });
30408
+ return {
30409
+ apiUrl: yield* Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(Config.withDefault(new URL("https://api.ignotum.cloud"))),
30410
+ token: credential.token
30411
+ };
30222
30412
  }));
30223
30413
  };
30224
30414
  var ControlApiError = class extends Schema.TaggedError()("ControlApiError", {
@@ -30232,7 +30422,7 @@ var ControlClientUnavailable = class extends Schema.TaggedError()("ControlClient
30232
30422
  }) {};
30233
30423
  var ControlClient = class extends Context.Service()("ignotum/cli/control-client/ControlClient") {
30234
30424
  static get layer() {
30235
- return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer));
30425
+ return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer.pipe(Layer.provide(AuthCredentialStore.layer))));
30236
30426
  }
30237
30427
  };
30238
30428
  const unavailable = (message, cause) => ControlClientUnavailable.make({
@@ -33603,11 +33793,11 @@ Schema.Struct({ fields: Schema.String });
33603
33793
  const JsonObjectString = Schema.fromJsonString(Schema.JsonObject);
33604
33794
  const LocalApplicationFiles = Context.Reference("ignotum/dev-runtime/files/LocalApplicationFiles", { defaultValue: () => ({
33605
33795
  admit: (_invocationId, _functionAddress, args) => Effect.succeed(encodedFileOccurrencesOf(args)),
33606
- grants: (_subscriptionId, files) => Effect.succeed(files.map((file) => ({
33607
- ...file,
33796
+ grants: (_subscriptionId, files) => Effect.succeed(Array.from(new Set(files.map(({ file }) => file.id)), (id) => ({
33797
+ id,
33608
33798
  url: ""
33609
33799
  }))),
33610
- prepare: (_invocationId, _functionAddress, _args, files) => Effect.succeed(files),
33800
+ prepare: (_invocationId, _functionAddress, _args, fileIds) => Effect.succeed(fileIds.map((id) => ({ id }))),
33611
33801
  upload: () => Effect.die("Local application file storage is unavailable."),
33612
33802
  releaseGrants: () => Effect.void,
33613
33803
  readGrant: () => Effect.succeed(Option.none()),
@@ -33634,23 +33824,26 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33634
33824
  const uploadFiles = /* @__PURE__ */ new Map();
33635
33825
  const grants = /* @__PURE__ */ new Map();
33636
33826
  const objectPath = (fileId) => path.join(directory, fileId);
33637
- const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, files) {
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.");
33638
33833
  const existing = preparations.get(invocationId);
33639
33834
  if (existing !== void 0) {
33640
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.");
33641
- return files.map((occurrence) => {
33642
- const token = existing.uploads.get(occurrence.file.id);
33643
- return token === void 0 ? occurrence : {
33644
- ...occurrence,
33836
+ return fileIds.map((id) => {
33837
+ const token = existing.uploads.get(id);
33838
+ return token === void 0 ? { id } : {
33839
+ id,
33645
33840
  url: `${fileUploadUrlPrefix}${token}`
33646
33841
  };
33647
33842
  });
33648
33843
  }
33649
- const all = new Map(encodedFileOccurrencesOf(args).map((occurrence) => [encodeCanonicalJson(occurrence.path), occurrence.file]));
33650
33844
  const distinct = /* @__PURE__ */ new Map();
33651
33845
  for (const occurrence of files) {
33652
- const arg = all.get(encodeCanonicalJson(occurrence.path));
33653
- if (arg === void 0 || !sameFile(arg, occurrence.file) || !validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
33846
+ if (!validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
33654
33847
  const prior = distinct.get(occurrence.file.id);
33655
33848
  if (prior !== void 0 && !sameFile(prior, occurrence.file)) throw new Error("One file ID has conflicting metadata.");
33656
33849
  distinct.set(occurrence.file.id, occurrence.file);
@@ -33696,10 +33889,10 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33696
33889
  files,
33697
33890
  uploads
33698
33891
  });
33699
- return files.map((occurrence) => {
33700
- const token = uploads.get(occurrence.file.id);
33701
- return token === void 0 ? occurrence : {
33702
- ...occurrence,
33892
+ return fileIds.map((id) => {
33893
+ const token = uploads.get(id);
33894
+ return token === void 0 ? { id } : {
33895
+ id,
33703
33896
  url: `${fileUploadUrlPrefix}${token}`
33704
33897
  };
33705
33898
  });
@@ -33738,9 +33931,9 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33738
33931
  file: occurrence.file
33739
33932
  });
33740
33933
  }
33741
- return occurrences.map((occurrence) => ({
33742
- ...occurrence,
33743
- url: `${fileGrantUrlPrefix}${tokens.get(occurrence.file.id)}`
33934
+ return Array.from(tokens, ([id, token]) => ({
33935
+ id,
33936
+ url: `${fileGrantUrlPrefix}${token}`
33744
33937
  }));
33745
33938
  });
33746
33939
  const reconcile = Effect.gen(function* () {
@@ -33844,14 +34037,14 @@ const operationForSubscription = (subscriptionId) => ({
33844
34037
  type: "Subscription",
33845
34038
  id: subscriptionId
33846
34039
  });
33847
- const protocolError = (code, message, operation) => {
34040
+ const syncError = (code, message, operation) => {
33848
34041
  if (operation === void 0) return {
33849
- type: "ProtocolError",
34042
+ type: "Error",
33850
34043
  code,
33851
34044
  message
33852
34045
  };
33853
34046
  return {
33854
- type: "ProtocolError",
34047
+ type: "Error",
33855
34048
  code,
33856
34049
  message,
33857
34050
  operation
@@ -33873,7 +34066,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33873
34066
  });
33874
34067
  const sendResolutionError = (operation, error, deliver = send) => {
33875
34068
  const code = error._tag;
33876
- return deliver(protocolError(code, error.message, operation));
34069
+ return deliver(syncError(code, error.message, operation));
33877
34070
  };
33878
34071
  const isActive = (subscriptionId, subscription) => Ref.get(subscriptions).pipe(Effect.map((current) => Option.getOrUndefined(HashMap.get(current, subscriptionId)) === subscription));
33879
34072
  const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (subscriptionId, subscription, message) {
@@ -33938,12 +34131,12 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33938
34131
  const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
33939
34132
  const operation = operationForSubscription(message.id);
33940
34133
  if (encodedFileOccurrencesOf(message.args).length > 0) {
33941
- yield* send(protocolError("InvalidArguments", "Queries cannot take files as arguments.", operation));
34134
+ yield* send(syncError("InvalidArguments", "Queries cannot take files as arguments.", operation));
33942
34135
  return;
33943
34136
  }
33944
34137
  const current = yield* Ref.get(subscriptions);
33945
34138
  if (HashMap.has(current, message.id)) {
33946
- yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
34139
+ yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
33947
34140
  return;
33948
34141
  }
33949
34142
  const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
@@ -33967,7 +34160,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33967
34160
  type: "Invocation",
33968
34161
  id: message.id
33969
34162
  };
33970
- if (!(yield* files.admit(message.id, message.function, message.args).pipe(Effect.as(true), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be admitted: ${String(cause)}`, operation)).pipe(Effect.as(false)))))) {
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)))))) {
33971
34164
  yield* files.releasePreparation(message.id);
33972
34165
  return;
33973
34166
  }
@@ -33998,25 +34191,26 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33998
34191
  invalidations: result.invalidations,
33999
34192
  committedRevision: result.committedRevision
34000
34193
  })));
34001
- }), Effect.catchTags({ InvocationIdConflict: () => send(protocolError("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({
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({
34002
34195
  cause,
34003
34196
  function: message.function,
34004
34197
  requestId: message.id
34005
34198
  }))), FiberSet.run(invocationFibers), Effect.asVoid);
34006
34199
  });
34007
- const handlePrepareMutation = Effect.fn("SyncServer.handlePrepareMutation")(function* (message) {
34200
+ const handlePrepare = Effect.fn("SyncServer.handlePrepare")(function* (message) {
34008
34201
  const operation = {
34009
34202
  type: "Invocation",
34010
34203
  id: message.id
34011
34204
  };
34012
34205
  yield* files.prepare(message.id, message.function, message.args, message.files).pipe(Effect.flatMap((uploads) => send({
34013
- type: "MutationPrepared",
34206
+ type: "Preparation",
34014
34207
  id: message.id,
34208
+ kind: message.kind,
34015
34209
  uploads
34016
- })), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34210
+ })), Effect.catchCause((cause) => send(syncError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34017
34211
  });
34018
34212
  const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
34019
- const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
34213
+ const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(syncError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
34020
34214
  if (message === void 0) return;
34021
34215
  switch (message.type) {
34022
34216
  case "Subscribe":
@@ -34027,8 +34221,8 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34027
34221
  yield* Ref.update(subscriptions, HashMap.remove(message.id));
34028
34222
  yield* files.releaseGrants(message.id);
34029
34223
  return;
34030
- case "PrepareMutation":
34031
- yield* handlePrepareMutation(message);
34224
+ case "Prepare":
34225
+ yield* handlePrepare(message);
34032
34226
  return;
34033
34227
  case "Invoke":
34034
34228
  yield* handleInvoke(message);
@@ -34274,11 +34468,11 @@ var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotu
34274
34468
  //#endregion
34275
34469
  //#region src/cli/agent-files.ts
34276
34470
  const references = [
34277
- ["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_API_TOKEN>\n```\n\nTreat the token as an account credential. Do not include it in client code or send it from a\ndeployed app. The API does not enable browser CORS, so call it from a CLI, CI job, or other trusted\nserver 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"],
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"],
34278
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"],
34279
- ["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 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## Environment variables\n\n| Variable | Meaning |\n| ------------------- | ------------------------------------------------------ |\n| `IGNOTUM_API_TOKEN` | Account token required by `deploy`. |\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"],
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"],
34280
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"],
34281
- ["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\nSet the API token you were issued:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\n```\n\nKeep the token outside the app directory and source control. It can create deployments for every\napp available to its account.\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`. It does not write the token\nthere. The generated `.gitignore` excludes `.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"],
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"],
34282
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"],
34283
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"],
34284
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"],
@@ -34297,7 +34491,7 @@ const references = [
34297
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"],
34298
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"],
34299
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"],
34300
- ["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\nSet the API token you were issued, then choose a globally available app slug:\n\n```sh\nexport IGNOTUM_API_TOKEN=your-token\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 token handling, slug rules, and deployment failure behavior.\n\nNext: [How Ignotum works](how-ignotum-works.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"],
34301
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"],
34302
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"],
34303
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"],
@@ -34509,9 +34703,10 @@ and updates them when the schema or server functions change.
34509
34703
 
34510
34704
  ## Deploy
34511
34705
 
34512
- Set \`IGNOTUM_API_TOKEN\`, then choose a unique slug on the first deployment:
34706
+ Sign in, then choose a unique slug on the first deployment:
34513
34707
 
34514
34708
  \`\`\`sh
34709
+ npx ignotum auth login
34515
34710
  npx ignotum deploy --app my-app
34516
34711
  \`\`\`
34517
34712
 
@@ -34662,6 +34857,40 @@ const DevPort = Schema.Int.check(Schema.isBetween({
34662
34857
  minimum: 1,
34663
34858
  maximum: 65535
34664
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
+ ]));
34665
34894
  const codegen = Command.make("codegen", {}, Effect.fn("codegen")(function* () {
34666
34895
  const path = yield* Path$1.Path;
34667
34896
  const terminal = yield* Terminal.Terminal;
@@ -34748,10 +34977,11 @@ const ignotum = Command.make("ignotum").pipe(Command.withSubcommands([
34748
34977
  install,
34749
34978
  codegen,
34750
34979
  dev,
34751
- deploy
34980
+ deploy,
34981
+ auth
34752
34982
  ]));
34753
34983
  const main = () => {
34754
- 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);
34755
34985
  };
34756
34986
  //#endregion
34757
34987
  //#region src/cli/bin.ts