ignotum 0.0.8 → 0.0.10

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 (44) hide show
  1. package/README.md +6 -2
  2. package/dist/cli/bin.mjs +295 -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-BhD-WQeO.js} +4 -3
  11. package/dist/runtime/descriptor-C5VA9qRl-BhD-WQeO.js.map +1 -0
  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-C00wobuy.js} +3 -3
  27. package/dist/runtime/{schema-DJfwfq87.js.map → schema-C00wobuy.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/descriptor-C5VA9qRl-C338iC6l.js.map +0 -1
  43. package/dist/runtime/file-BXf63ulU.js.map +0 -1
  44. 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");
@@ -29537,6 +29547,7 @@ const TransportValueSchema = Schema.suspend(() => Schema.Union([
29537
29547
  Schema.Array(TransportValueSchema),
29538
29548
  Schema.Record(Schema.String, Schema.UndefinedOr(TransportValueSchema))
29539
29549
  ]));
29550
+ Schema.toType(TransportValueSchema);
29540
29551
  const collectDatePaths = (value, path, paths) => {
29541
29552
  if (Predicate.isDate(value)) {
29542
29553
  Schema.decodeSync(Schema.Date)(value);
@@ -29596,18 +29607,18 @@ const Invoke = Schema.Struct({
29596
29607
  function: FunctionAddress,
29597
29608
  args: Schema.Json
29598
29609
  });
29599
- const PrepareMutation = Schema.Struct({
29600
- type: Schema.Literal("PrepareMutation"),
29610
+ const Prepare = Schema.Struct({
29611
+ type: Schema.Literal("Prepare"),
29601
29612
  id: InvocationId,
29602
29613
  kind: Schema.Literal("Mutation"),
29603
29614
  function: FunctionAddress,
29604
29615
  args: Schema.Json,
29605
- files: Schema.Array(RuntimeFileOccurrence)
29616
+ files: Schema.Array(FileId)
29606
29617
  });
29607
29618
  const ClientMessage = Schema.Union([
29608
29619
  Subscribe,
29609
29620
  Unsubscribe,
29610
- PrepareMutation,
29621
+ Prepare,
29611
29622
  Invoke
29612
29623
  ]);
29613
29624
  const SubscriptionOperation = Schema.Struct({
@@ -29644,17 +29655,16 @@ const Snapshot = Schema.Struct({
29644
29655
  result: WireResult,
29645
29656
  revision: AppStateRevision,
29646
29657
  files: Schema.optional(Schema.Array(Schema.Struct({
29647
- path: RuntimeFileOccurrence.fields.path,
29648
- file: RuntimeFileMetadata,
29658
+ id: FileId,
29649
29659
  url: Schema.String
29650
29660
  })))
29651
29661
  });
29652
- const MutationPrepared = Schema.Struct({
29653
- type: Schema.Literal("MutationPrepared"),
29662
+ const Preparation = Schema.Struct({
29663
+ type: Schema.Literal("Preparation"),
29654
29664
  id: InvocationId,
29665
+ kind: Schema.Literal("Mutation"),
29655
29666
  uploads: Schema.Array(Schema.Struct({
29656
- path: RuntimeFileOccurrence.fields.path,
29657
- file: RuntimeFileMetadata,
29667
+ id: FileId,
29658
29668
  url: Schema.optional(Schema.String)
29659
29669
  }))
29660
29670
  });
@@ -29669,7 +29679,7 @@ const SyncResultFailure = Schema.Struct({
29669
29679
  id: InvocationId,
29670
29680
  result: WireFailure
29671
29681
  });
29672
- const ProtocolErrorCode = Schema.Literals([
29682
+ const ErrorCode = Schema.Literals([
29673
29683
  "DuplicateOperationId",
29674
29684
  "FunctionUnavailable",
29675
29685
  "InvalidArguments",
@@ -29679,25 +29689,25 @@ const ProtocolErrorCode = Schema.Literals([
29679
29689
  "UnknownFunction",
29680
29690
  "WrongFunctionKind"
29681
29691
  ]);
29682
- const ProtocolError = Schema.Struct({
29683
- type: Schema.Literal("ProtocolError"),
29692
+ const SyncError = Schema.Struct({
29693
+ type: Schema.Literal("Error"),
29684
29694
  operation: Schema.optional(Operation),
29685
- code: ProtocolErrorCode,
29695
+ code: ErrorCode,
29686
29696
  message: Schema.String
29687
29697
  });
29688
- const DeploymentChanged = Schema.Struct({
29689
- type: Schema.Literal("DeploymentChanged"),
29698
+ const Deployment = Schema.Struct({
29699
+ type: Schema.Literal("Deployment"),
29690
29700
  deploymentId: DeploymentId,
29691
29701
  generation: DeploymentGeneration
29692
29702
  });
29693
29703
  const ServerMessage = Schema.Union([
29694
29704
  SyncHandshake,
29695
29705
  Snapshot,
29696
- MutationPrepared,
29706
+ Preparation,
29697
29707
  SyncResultSuccess,
29698
29708
  SyncResultFailure,
29699
- ProtocolError,
29700
- DeploymentChanged
29709
+ SyncError,
29710
+ Deployment
29701
29711
  ]);
29702
29712
  const ClientMessageJson = Schema.fromJsonString(ClientMessage);
29703
29713
  const ServerMessageJson = Schema.fromJsonString(ServerMessage);
@@ -30080,7 +30090,7 @@ var IdGenerator = class IdGenerator extends Context.Service()("@ignotum/shared/i
30080
30090
  };
30081
30091
  //#endregion
30082
30092
  //#region package.json
30083
- var version = "0.0.8";
30093
+ var version = "0.0.10";
30084
30094
  //#endregion
30085
30095
  //#region src/cli/codegen.ts
30086
30096
  const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
@@ -30208,6 +30218,183 @@ const generate = Effect.fn("Codegen.generate")(function* (appDirectory) {
30208
30218
  written
30209
30219
  };
30210
30220
  });
30221
+ const Credential = initial(Schema.Struct({
30222
+ formatVersion: Schema.Literal(1),
30223
+ authUrl: Schema.URLFromString,
30224
+ token: Schema.String
30225
+ }));
30226
+ const AuthUrl = Schema.URL.check(Schema.makeFilter((url) => url.protocol === "https:" || url.protocol === "http:" && [
30227
+ "127.0.0.1",
30228
+ "::1",
30229
+ "localhost"
30230
+ ].includes(url.hostname), { message: "The auth URL must use HTTPS unless it targets localhost." }));
30231
+ var AuthCommandError = class extends Schema.TaggedError()("AuthCommandError", {
30232
+ cause: Schema.optional(Schema.Defect()),
30233
+ message: Schema.String
30234
+ }) {};
30235
+ const commandError = (message, cause) => AuthCommandError.make(cause === void 0 ? { message } : {
30236
+ cause,
30237
+ message
30238
+ });
30239
+ var AuthCredentialStore = class AuthCredentialStore extends Context.Service()("ignotum/cli/auth-client/AuthCredentialStore") {
30240
+ static layer = Layer.effect(AuthCredentialStore, Effect.gen(function* () {
30241
+ const fileSystem = yield* FileSystem.FileSystem;
30242
+ const path = yield* Path$1.Path;
30243
+ const configured = yield* Config.option(Config.string("IGNOTUM_CONFIG_DIR"));
30244
+ const xdg = yield* Config.option(Config.string("XDG_CONFIG_HOME"));
30245
+ const appData = yield* Config.option(Config.string("APPDATA"));
30246
+ const configurationDirectory = Option.getOrElse(configured, () => Option.getOrElse(xdg, () => process$1.platform === "win32" && Option.isSome(appData) ? appData.value : path.join(homedir(), ".config")));
30247
+ const directory = path.join(configurationDirectory, "ignotum");
30248
+ const credentialPath = path.join(directory, "auth.json");
30249
+ const load = Effect.fn("AuthCredentialStore.load")(function* () {
30250
+ if (!(yield* fileSystem.exists(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not check the saved login.", cause))))) return;
30251
+ const text = yield* fileSystem.readFileString(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not read the saved login.", cause)));
30252
+ const credential = yield* Schema.decodeEffect(Schema.fromJsonString(Credential))(text).pipe(Effect.mapError((cause) => commandError("The saved login is invalid.", cause)));
30253
+ return {
30254
+ authUrl: credential.authUrl,
30255
+ token: Redacted.make(credential.token)
30256
+ };
30257
+ });
30258
+ const save = Effect.fn("AuthCredentialStore.save")(function* (credential) {
30259
+ const value = {
30260
+ formatVersion: 1,
30261
+ authUrl: credential.authUrl,
30262
+ token: Redacted.value(credential.token)
30263
+ };
30264
+ const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(Credential))(value).pipe(Effect.mapError((cause) => commandError("Could not encode the login.", cause)));
30265
+ yield* fileSystem.makeDirectory(directory, {
30266
+ mode: 448,
30267
+ recursive: true
30268
+ }).pipe(Effect.mapError((cause) => commandError("Could not create the config directory.", cause)));
30269
+ yield* Effect.scoped(Effect.gen(function* () {
30270
+ const temporaryPath = yield* fileSystem.makeTempFileScoped({
30271
+ directory,
30272
+ prefix: ".auth-",
30273
+ suffix: ".json"
30274
+ });
30275
+ yield* fileSystem.writeFileString(temporaryPath, `${encoded}\n`, { mode: 384 });
30276
+ yield* fileSystem.rename(temporaryPath, credentialPath);
30277
+ })).pipe(Effect.mapError((cause) => commandError("Could not save the login.", cause)));
30278
+ });
30279
+ const remove = Effect.fn("AuthCredentialStore.remove")(function* () {
30280
+ if (!(yield* fileSystem.exists(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not check the saved login.", cause))))) return false;
30281
+ yield* fileSystem.remove(credentialPath).pipe(Effect.mapError((cause) => commandError("Could not remove the saved login.", cause)));
30282
+ return true;
30283
+ });
30284
+ return AuthCredentialStore.of({
30285
+ load: load(),
30286
+ remove: remove(),
30287
+ save
30288
+ });
30289
+ }));
30290
+ };
30291
+ const makeClient = (authUrl) => createAuthClient({
30292
+ basePath: "/v1",
30293
+ baseURL: authUrl.href,
30294
+ plugins: [deviceAuthorizationClient(), organizationClient()]
30295
+ });
30296
+ const authorization = (token) => ({ Authorization: `Bearer ${Redacted.value(token)}` });
30297
+ const responseError = (error) => error.error_description ?? error.message ?? "Authentication failed.";
30298
+ const login = Effect.fn("Auth.login")(function* () {
30299
+ const store = yield* AuthCredentialStore;
30300
+ const terminal = yield* Terminal.Terminal;
30301
+ 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)));
30302
+ const client = makeClient(authUrl);
30303
+ const existing = yield* store.load;
30304
+ if (existing !== void 0 && existing.authUrl.href === authUrl.href) {
30305
+ const result = yield* Effect.tryPromise({
30306
+ try: () => client.getSession({ fetchOptions: { headers: authorization(existing.token) } }),
30307
+ catch: (cause) => commandError("Could not check the saved login.", cause)
30308
+ });
30309
+ if (result.data?.user !== void 0) return {
30310
+ email: result.data.user.email,
30311
+ verificationUrl: void 0
30312
+ };
30313
+ }
30314
+ const started = yield* Effect.tryPromise({
30315
+ try: () => client.device.code({
30316
+ client_id: "ignotum-cli",
30317
+ scope: "openid profile email"
30318
+ }),
30319
+ catch: (cause) => commandError("Could not start login.", cause)
30320
+ });
30321
+ if (started.data === null) return yield* commandError(responseError(started.error));
30322
+ const url = started.data.verification_uri_complete ?? started.data.verification_uri;
30323
+ yield* terminal.display(`Open ${started.data.verification_uri}\nCode: ${started.data.user_code}\n\nWaiting for approval...\n`);
30324
+ yield* Effect.tryPromise({
30325
+ try: async () => (await import("open")).default(url),
30326
+ catch: (cause) => commandError("Could not open the browser.", cause)
30327
+ }).pipe(Effect.ignore);
30328
+ let interval = started.data.interval ?? 5;
30329
+ while (true) {
30330
+ yield* Effect.sleep(`${interval} seconds`);
30331
+ const polled = yield* Effect.tryPromise({
30332
+ try: () => client.device.token({
30333
+ client_id: "ignotum-cli",
30334
+ device_code: started.data.device_code,
30335
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
30336
+ }),
30337
+ catch: (cause) => commandError("Login polling failed.", cause)
30338
+ });
30339
+ if (polled.data?.access_token !== void 0) {
30340
+ const token = Redacted.make(polled.data.access_token);
30341
+ const session = yield* Effect.tryPromise({
30342
+ try: () => client.getSession({ fetchOptions: { headers: authorization(token) } }),
30343
+ catch: (cause) => commandError("Could not read the new session.", cause)
30344
+ });
30345
+ if (session.data?.user === void 0) return yield* commandError("The auth service returned an invalid session.");
30346
+ yield* store.save({
30347
+ authUrl,
30348
+ token
30349
+ });
30350
+ return {
30351
+ email: session.data.user.email,
30352
+ verificationUrl: url
30353
+ };
30354
+ }
30355
+ switch (polled.error?.error) {
30356
+ case "authorization_pending": break;
30357
+ case "slow_down":
30358
+ interval += 5;
30359
+ break;
30360
+ case "access_denied": return yield* commandError("Login was denied.");
30361
+ case "expired_token": return yield* commandError("The login code expired. Run login again.");
30362
+ default: return yield* commandError(polled.error === null ? "Login failed." : responseError(polled.error));
30363
+ }
30364
+ }
30365
+ });
30366
+ const logout = Effect.fn("Auth.logout")(function* () {
30367
+ const store = yield* AuthCredentialStore;
30368
+ const credential = yield* store.load;
30369
+ if (credential === void 0) return false;
30370
+ const client = makeClient(credential.authUrl);
30371
+ const result = yield* Effect.tryPromise({
30372
+ try: () => client.signOut({ fetchOptions: { headers: authorization(credential.token) } }),
30373
+ catch: (cause) => commandError("Could not revoke the session.", cause)
30374
+ });
30375
+ if (result.error !== null) return yield* commandError(responseError(result.error));
30376
+ yield* store.remove;
30377
+ return true;
30378
+ });
30379
+ const status = Effect.fn("Auth.status")(function* () {
30380
+ const credential = yield* (yield* AuthCredentialStore).load;
30381
+ if (credential === void 0) return void 0;
30382
+ const client = makeClient(credential.authUrl);
30383
+ const session = yield* Effect.tryPromise({
30384
+ try: () => client.getSession({ fetchOptions: { headers: authorization(credential.token) } }),
30385
+ catch: (cause) => commandError("Could not check login status.", cause)
30386
+ });
30387
+ if (session.data?.user === void 0) return void 0;
30388
+ const teams = yield* Effect.tryPromise({
30389
+ try: () => client.organization.list({ fetchOptions: { headers: authorization(credential.token) } }),
30390
+ catch: (cause) => commandError("Could not load the team.", cause)
30391
+ });
30392
+ return {
30393
+ email: session.data.user.email,
30394
+ name: session.data.user.name,
30395
+ teamName: teams.data?.[0]?.name
30396
+ };
30397
+ });
30211
30398
  //#endregion
30212
30399
  //#region src/cli/control-client.ts
30213
30400
  const isLoopbackAddress = (hostname) => {
@@ -30216,9 +30403,13 @@ const isLoopbackAddress = (hostname) => {
30216
30403
  };
30217
30404
  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
30405
  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")
30406
+ static layer = Layer.effect(HostedControlConfiguration, Effect.gen(function* () {
30407
+ const credential = yield* (yield* AuthCredentialStore).load;
30408
+ if (credential === void 0) return yield* AuthCommandError.make({ message: "Run 'npx ignotum auth login' first." });
30409
+ return {
30410
+ apiUrl: yield* Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(Config.withDefault(new URL("https://api.ignotum.cloud"))),
30411
+ token: credential.token
30412
+ };
30222
30413
  }));
30223
30414
  };
30224
30415
  var ControlApiError = class extends Schema.TaggedError()("ControlApiError", {
@@ -30232,7 +30423,7 @@ var ControlClientUnavailable = class extends Schema.TaggedError()("ControlClient
30232
30423
  }) {};
30233
30424
  var ControlClient = class extends Context.Service()("ignotum/cli/control-client/ControlClient") {
30234
30425
  static get layer() {
30235
- return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer));
30426
+ return controlClientLayer.pipe(Layer.provideMerge(HostedControlConfiguration.layer.pipe(Layer.provide(AuthCredentialStore.layer))));
30236
30427
  }
30237
30428
  };
30238
30429
  const unavailable = (message, cause) => ControlClientUnavailable.make({
@@ -33603,11 +33794,11 @@ Schema.Struct({ fields: Schema.String });
33603
33794
  const JsonObjectString = Schema.fromJsonString(Schema.JsonObject);
33604
33795
  const LocalApplicationFiles = Context.Reference("ignotum/dev-runtime/files/LocalApplicationFiles", { defaultValue: () => ({
33605
33796
  admit: (_invocationId, _functionAddress, args) => Effect.succeed(encodedFileOccurrencesOf(args)),
33606
- grants: (_subscriptionId, files) => Effect.succeed(files.map((file) => ({
33607
- ...file,
33797
+ grants: (_subscriptionId, files) => Effect.succeed(Array.from(new Set(files.map(({ file }) => file.id)), (id) => ({
33798
+ id,
33608
33799
  url: ""
33609
33800
  }))),
33610
- prepare: (_invocationId, _functionAddress, _args, files) => Effect.succeed(files),
33801
+ prepare: (_invocationId, _functionAddress, _args, fileIds) => Effect.succeed(fileIds.map((id) => ({ id }))),
33611
33802
  upload: () => Effect.die("Local application file storage is unavailable."),
33612
33803
  releaseGrants: () => Effect.void,
33613
33804
  readGrant: () => Effect.succeed(Option.none()),
@@ -33634,23 +33825,26 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33634
33825
  const uploadFiles = /* @__PURE__ */ new Map();
33635
33826
  const grants = /* @__PURE__ */ new Map();
33636
33827
  const objectPath = (fileId) => path.join(directory, fileId);
33637
- const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, files) {
33828
+ const prepare = Effect.fn("LocalApplicationFiles.prepare")(function* (invocationId, functionAddress, args, fileIds) {
33829
+ if (fileIds.length === 0) throw new Error("A file preparation must contain a file ID.");
33830
+ const selected = new Set(fileIds);
33831
+ if (selected.size !== fileIds.length) throw new Error("A file preparation contains a duplicate file ID.");
33832
+ const files = encodedFileOccurrencesOf(args).filter((occurrence) => selected.has(occurrence.file.id));
33833
+ if (new Set(files.map((occurrence) => occurrence.file.id)).size !== selected.size) throw new Error("Prepared files do not match the mutation arguments.");
33638
33834
  const existing = preparations.get(invocationId);
33639
33835
  if (existing !== void 0) {
33640
33836
  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,
33837
+ return fileIds.map((id) => {
33838
+ const token = existing.uploads.get(id);
33839
+ return token === void 0 ? { id } : {
33840
+ id,
33645
33841
  url: `${fileUploadUrlPrefix}${token}`
33646
33842
  };
33647
33843
  });
33648
33844
  }
33649
- const all = new Map(encodedFileOccurrencesOf(args).map((occurrence) => [encodeCanonicalJson(occurrence.path), occurrence.file]));
33650
33845
  const distinct = /* @__PURE__ */ new Map();
33651
33846
  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.");
33847
+ if (!validFilename(occurrence.file.name)) throw new Error("Prepared files do not match the mutation arguments.");
33654
33848
  const prior = distinct.get(occurrence.file.id);
33655
33849
  if (prior !== void 0 && !sameFile(prior, occurrence.file)) throw new Error("One file ID has conflicting metadata.");
33656
33850
  distinct.set(occurrence.file.id, occurrence.file);
@@ -33696,10 +33890,10 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33696
33890
  files,
33697
33891
  uploads
33698
33892
  });
33699
- return files.map((occurrence) => {
33700
- const token = uploads.get(occurrence.file.id);
33701
- return token === void 0 ? occurrence : {
33702
- ...occurrence,
33893
+ return fileIds.map((id) => {
33894
+ const token = uploads.get(id);
33895
+ return token === void 0 ? { id } : {
33896
+ id,
33703
33897
  url: `${fileUploadUrlPrefix}${token}`
33704
33898
  };
33705
33899
  });
@@ -33738,9 +33932,9 @@ const localApplicationFilesLayer = (appDirectory) => Layer.effect(LocalApplicati
33738
33932
  file: occurrence.file
33739
33933
  });
33740
33934
  }
33741
- return occurrences.map((occurrence) => ({
33742
- ...occurrence,
33743
- url: `${fileGrantUrlPrefix}${tokens.get(occurrence.file.id)}`
33935
+ return Array.from(tokens, ([id, token]) => ({
33936
+ id,
33937
+ url: `${fileGrantUrlPrefix}${token}`
33744
33938
  }));
33745
33939
  });
33746
33940
  const reconcile = Effect.gen(function* () {
@@ -33844,14 +34038,14 @@ const operationForSubscription = (subscriptionId) => ({
33844
34038
  type: "Subscription",
33845
34039
  id: subscriptionId
33846
34040
  });
33847
- const protocolError = (code, message, operation) => {
34041
+ const syncError = (code, message, operation) => {
33848
34042
  if (operation === void 0) return {
33849
- type: "ProtocolError",
34043
+ type: "Error",
33850
34044
  code,
33851
34045
  message
33852
34046
  };
33853
34047
  return {
33854
- type: "ProtocolError",
34048
+ type: "Error",
33855
34049
  code,
33856
34050
  message,
33857
34051
  operation
@@ -33873,7 +34067,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33873
34067
  });
33874
34068
  const sendResolutionError = (operation, error, deliver = send) => {
33875
34069
  const code = error._tag;
33876
- return deliver(protocolError(code, error.message, operation));
34070
+ return deliver(syncError(code, error.message, operation));
33877
34071
  };
33878
34072
  const isActive = (subscriptionId, subscription) => Ref.get(subscriptions).pipe(Effect.map((current) => Option.getOrUndefined(HashMap.get(current, subscriptionId)) === subscription));
33879
34073
  const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (subscriptionId, subscription, message) {
@@ -33938,12 +34132,12 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33938
34132
  const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
33939
34133
  const operation = operationForSubscription(message.id);
33940
34134
  if (encodedFileOccurrencesOf(message.args).length > 0) {
33941
- yield* send(protocolError("InvalidArguments", "Queries cannot take files as arguments.", operation));
34135
+ yield* send(syncError("InvalidArguments", "Queries cannot take files as arguments.", operation));
33942
34136
  return;
33943
34137
  }
33944
34138
  const current = yield* Ref.get(subscriptions);
33945
34139
  if (HashMap.has(current, message.id)) {
33946
- yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
34140
+ yield* send(syncError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
33947
34141
  return;
33948
34142
  }
33949
34143
  const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
@@ -33967,7 +34161,7 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33967
34161
  type: "Invocation",
33968
34162
  id: message.id
33969
34163
  };
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)))))) {
34164
+ 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
34165
  yield* files.releasePreparation(message.id);
33972
34166
  return;
33973
34167
  }
@@ -33998,25 +34192,26 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
33998
34192
  invalidations: result.invalidations,
33999
34193
  committedRevision: result.committedRevision
34000
34194
  })));
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({
34195
+ }), 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
34196
  cause,
34003
34197
  function: message.function,
34004
34198
  requestId: message.id
34005
34199
  }))), FiberSet.run(invocationFibers), Effect.asVoid);
34006
34200
  });
34007
- const handlePrepareMutation = Effect.fn("SyncServer.handlePrepareMutation")(function* (message) {
34201
+ const handlePrepare = Effect.fn("SyncServer.handlePrepare")(function* (message) {
34008
34202
  const operation = {
34009
34203
  type: "Invocation",
34010
34204
  id: message.id
34011
34205
  };
34012
34206
  yield* files.prepare(message.id, message.function, message.args, message.files).pipe(Effect.flatMap((uploads) => send({
34013
- type: "MutationPrepared",
34207
+ type: "Preparation",
34014
34208
  id: message.id,
34209
+ kind: message.kind,
34015
34210
  uploads
34016
- })), Effect.catchCause((cause) => send(protocolError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34211
+ })), Effect.catchCause((cause) => send(syncError("InvalidArguments", `The mutation files could not be prepared: ${String(cause)}`, operation))));
34017
34212
  });
34018
34213
  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."))));
34214
+ const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(syncError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
34020
34215
  if (message === void 0) return;
34021
34216
  switch (message.type) {
34022
34217
  case "Subscribe":
@@ -34027,8 +34222,8 @@ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
34027
34222
  yield* Ref.update(subscriptions, HashMap.remove(message.id));
34028
34223
  yield* files.releaseGrants(message.id);
34029
34224
  return;
34030
- case "PrepareMutation":
34031
- yield* handlePrepareMutation(message);
34225
+ case "Prepare":
34226
+ yield* handlePrepare(message);
34032
34227
  return;
34033
34228
  case "Invoke":
34034
34229
  yield* handleInvoke(message);
@@ -34274,11 +34469,11 @@ var SKILL_default = "---\nname: ignotum\ndescription: Build and modify an Ignotu
34274
34469
  //#endregion
34275
34470
  //#region src/cli/agent-files.ts
34276
34471
  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"],
34472
+ ["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
34473
  ["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"],
34474
+ ["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
34475
  ["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"],
34476
+ ["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
34477
  ["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
34478
  ["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
34479
  ["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 +34492,7 @@ const references = [
34297
34492
  ["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
34493
  ["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
34494
  ["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"],
34495
+ ["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
34496
  ["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
34497
  ["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
34498
  ["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 +34704,10 @@ and updates them when the schema or server functions change.
34509
34704
 
34510
34705
  ## Deploy
34511
34706
 
34512
- Set \`IGNOTUM_API_TOKEN\`, then choose a unique slug on the first deployment:
34707
+ Sign in, then choose a unique slug on the first deployment:
34513
34708
 
34514
34709
  \`\`\`sh
34710
+ npx ignotum auth login
34515
34711
  npx ignotum deploy --app my-app
34516
34712
  \`\`\`
34517
34713
 
@@ -34662,6 +34858,40 @@ const DevPort = Schema.Int.check(Schema.isBetween({
34662
34858
  minimum: 1,
34663
34859
  maximum: 65535
34664
34860
  }));
34861
+ const authLogin = Command.make("login", {}, Effect.fn("auth login")(function* () {
34862
+ const terminal = yield* Terminal.Terminal;
34863
+ const result = yield* login().pipe(Effect.mapError((error) => CliError.UserError.make({
34864
+ cause: error,
34865
+ userMessage: error.message
34866
+ })));
34867
+ yield* terminal.display(`Signed in as ${result.email}.\n`);
34868
+ })).pipe(Command.withDescription("Sign in to Ignotum with GitHub."));
34869
+ const authLogout = Command.make("logout", {}, Effect.fn("auth logout")(function* () {
34870
+ const terminal = yield* Terminal.Terminal;
34871
+ const removed = yield* logout().pipe(Effect.mapError((error) => CliError.UserError.make({
34872
+ cause: error,
34873
+ userMessage: error.message
34874
+ })));
34875
+ yield* terminal.display(removed ? "Signed out.\n" : "Not signed in.\n");
34876
+ })).pipe(Command.withDescription("Revoke and remove the saved login."));
34877
+ const authStatus = Command.make("status", {}, Effect.fn("auth status")(function* () {
34878
+ const terminal = yield* Terminal.Terminal;
34879
+ const current = yield* status().pipe(Effect.mapError((error) => CliError.UserError.make({
34880
+ cause: error,
34881
+ userMessage: error.message
34882
+ })));
34883
+ if (current === void 0) {
34884
+ yield* terminal.display("Not signed in.\n");
34885
+ return;
34886
+ }
34887
+ const team = current.teamName === void 0 ? "" : `\nTeam: ${current.teamName}`;
34888
+ yield* terminal.display(`Signed in as ${current.name} (${current.email}).${team}\n`);
34889
+ })).pipe(Command.withDescription("Show the current Ignotum login."));
34890
+ const auth = Command.make("auth").pipe(Command.withDescription("Manage your Ignotum login."), Command.withSubcommands([
34891
+ authLogin,
34892
+ authLogout,
34893
+ authStatus
34894
+ ]));
34665
34895
  const codegen = Command.make("codegen", {}, Effect.fn("codegen")(function* () {
34666
34896
  const path = yield* Path$1.Path;
34667
34897
  const terminal = yield* Terminal.Terminal;
@@ -34748,10 +34978,11 @@ const ignotum = Command.make("ignotum").pipe(Command.withSubcommands([
34748
34978
  install,
34749
34979
  codegen,
34750
34980
  dev,
34751
- deploy
34981
+ deploy,
34982
+ auth
34752
34983
  ]));
34753
34984
  const main = () => {
34754
- ignotum.pipe(Command.run({ version }), Effect.provide(Layer.mergeAll(layer$1, layerUndici, IdGenerator.layer)), runMain);
34985
+ ignotum.pipe(Command.run({ version }), Effect.provide(Layer.mergeAll(layer$1, layerUndici, IdGenerator.layer, AuthCredentialStore.layer.pipe(Layer.provide(layer$1)))), runMain);
34755
34986
  };
34756
34987
  //#endregion
34757
34988
  //#region src/cli/bin.ts