primitive-admin 1.1.0-alpha.82 → 1.1.0-alpha.83

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 (86) hide show
  1. package/dist/src/commands/documents.js +333 -42
  2. package/dist/src/commands/documents.js.map +1 -1
  3. package/dist/src/commands/functions.js +302 -120
  4. package/dist/src/commands/functions.js.map +1 -1
  5. package/dist/src/commands/locks.js +15 -1
  6. package/dist/src/commands/locks.js.map +1 -1
  7. package/dist/src/commands/sync.d.ts +50 -0
  8. package/dist/src/commands/sync.js +560 -312
  9. package/dist/src/commands/sync.js.map +1 -1
  10. package/dist/src/lib/api-client.d.ts +75 -2
  11. package/dist/src/lib/api-client.js +122 -7
  12. package/dist/src/lib/api-client.js.map +1 -1
  13. package/dist/src/lib/config-object-descriptor.js +5 -5
  14. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  15. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  16. package/dist/src/lib/document-ingest-artifact.js +505 -0
  17. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  18. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  19. package/dist/src/lib/document-ingest-input.js +132 -0
  20. package/dist/src/lib/document-ingest-input.js.map +1 -0
  21. package/dist/src/lib/document-ingest.d.ts +78 -0
  22. package/dist/src/lib/document-ingest.js +231 -0
  23. package/dist/src/lib/document-ingest.js.map +1 -0
  24. package/dist/src/lib/function-bundle.d.ts +6 -0
  25. package/dist/src/lib/function-bundle.js +8 -0
  26. package/dist/src/lib/function-bundle.js.map +1 -1
  27. package/dist/src/lib/function-collect.d.ts +1 -1
  28. package/dist/src/lib/function-collect.js +16 -0
  29. package/dist/src/lib/function-collect.js.map +1 -1
  30. package/dist/src/lib/function-db-types.d.ts +4 -0
  31. package/dist/src/lib/function-db-types.js +31 -2
  32. package/dist/src/lib/function-db-types.js.map +1 -1
  33. package/dist/src/lib/function-document-types.d.ts +83 -0
  34. package/dist/src/lib/function-document-types.js +133 -0
  35. package/dist/src/lib/function-document-types.js.map +1 -1
  36. package/dist/src/lib/function-grants-preflight.js +5 -0
  37. package/dist/src/lib/function-grants-preflight.js.map +1 -1
  38. package/dist/src/lib/function-log-lines.d.ts +76 -0
  39. package/dist/src/lib/function-log-lines.js +160 -0
  40. package/dist/src/lib/function-log-lines.js.map +1 -0
  41. package/dist/src/lib/function-log-row.d.ts +29 -0
  42. package/dist/src/lib/function-log-row.js +73 -0
  43. package/dist/src/lib/function-log-row.js.map +1 -0
  44. package/dist/src/lib/function-run.d.ts +53 -35
  45. package/dist/src/lib/function-run.js +55 -44
  46. package/dist/src/lib/function-run.js.map +1 -1
  47. package/dist/src/lib/function-schema-codegen.d.ts +6 -9
  48. package/dist/src/lib/function-schema-codegen.js +22 -35
  49. package/dist/src/lib/function-schema-codegen.js.map +1 -1
  50. package/dist/src/lib/function-sync.d.ts +226 -35
  51. package/dist/src/lib/function-sync.js +731 -99
  52. package/dist/src/lib/function-sync.js.map +1 -1
  53. package/dist/src/lib/function-versions.d.ts +122 -0
  54. package/dist/src/lib/function-versions.js +182 -0
  55. package/dist/src/lib/function-versions.js.map +1 -0
  56. package/dist/src/lib/generated-config-surfaces.d.ts +390 -146
  57. package/dist/src/lib/generated-config-surfaces.js +1207 -401
  58. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  59. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  60. package/dist/src/lib/generated-sdk-types.js +1 -1
  61. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  62. package/dist/src/lib/log-inspection.d.ts +2 -0
  63. package/dist/src/lib/log-inspection.js +4 -0
  64. package/dist/src/lib/log-inspection.js.map +1 -1
  65. package/dist/src/lib/output.d.ts +15 -0
  66. package/dist/src/lib/output.js +28 -0
  67. package/dist/src/lib/output.js.map +1 -1
  68. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  69. package/dist/src/lib/snapshot-audit-source.js +58 -0
  70. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  71. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  72. package/dist/src/lib/snapshot-audit-store.js +196 -0
  73. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  74. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  75. package/dist/src/lib/snapshot-audit.js +431 -0
  76. package/dist/src/lib/snapshot-audit.js.map +1 -0
  77. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  78. package/dist/src/lib/snapshot-build-rows.js +87 -0
  79. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  80. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +23 -16
  81. package/dist/src/lib/swift-codegen/functionGenerator.js +34 -33
  82. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
  83. package/dist/src/lib/sync-selectors.d.ts +45 -2
  84. package/dist/src/lib/sync-selectors.js +66 -5
  85. package/dist/src/lib/sync-selectors.js.map +1 -1
  86. package/package.json +2 -2
@@ -1,15 +1,56 @@
1
+ import { snapshotBuildRows } from "../lib/snapshot-build-rows.js";
1
2
  import { ApiClient } from "../lib/api-client.js";
2
3
  import { resolveAppId } from "../lib/config.js";
3
- import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, } from "../lib/output.js";
4
+ import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, flushOutput, } from "../lib/output.js";
4
5
  import { confirmPrompt } from "../lib/confirm-prompt.js";
5
6
  import { parseDataOption } from "../lib/data-input.js";
6
7
  import { parseFilterOptions } from "../lib/record-filter.js";
7
8
  import { buildPermissionsExport } from "../lib/document-export-permissions.js";
8
9
  import { chunkAddressOf, renumberManifestForExport, } from "../lib/snapshot-manifest-layout.js";
9
10
  import { validateSnapshotManifest } from "js-bao";
11
+ import { auditSnapshotChain, } from "../lib/snapshot-audit.js";
12
+ import { createAuditApiSource } from "../lib/snapshot-audit-source.js";
13
+ import { createAuditLocalStore, createChainFingerprintReader, } from "../lib/snapshot-audit-store.js";
14
+ import { discoverIngestInput, } from "../lib/document-ingest-input.js";
15
+ import { buildIngestArtifact, ingestSchemaFromIntrospection, } from "../lib/document-ingest-artifact.js";
16
+ import { driveDocumentIngest, summariseIngestPlan, assertIngestConfirmable, } from "../lib/document-ingest.js";
10
17
  import { ulid } from "ulid";
11
18
  import * as fs from "fs";
12
19
  import * as path from "path";
20
+ /**
21
+ * One bulk-load session as rows (#3434, #3435).
22
+ *
23
+ * Shared by `documents ingests get` and by `documents ingest`'s own terminal
24
+ * report, so an operator reads the same thing however they arrived at it.
25
+ */
26
+ function printIngestSession(session) {
27
+ printResult("Session", session.sessionId);
28
+ printResult("Document", session.documentId);
29
+ printResult("State", session.state);
30
+ printResult("Created by", session.createdBy);
31
+ printResult("Created", formatDate(session.createdAt));
32
+ printResult("Updated", formatDate(session.updatedAt));
33
+ printResult("Completed", session.completedAt ? formatDate(session.completedAt) : "—");
34
+ printResult("Chunks", session.chunks);
35
+ printResult("Rows", session.rows);
36
+ printResult("Bytes", session.bytes);
37
+ const progress = session.progress ?? {};
38
+ printResult("Progress", `copied ${progress.rowsCopied ?? 0} rows, applied ` +
39
+ `${progress.chunksApplied ?? 0} chunks / ${progress.rowsApplied ?? 0} rows, ` +
40
+ `reconciled ${progress.recordsReconciled ?? 0} records`);
41
+ printResult("Copy point", session.copyStartedAt ? formatDate(session.copyStartedAt) : "—");
42
+ printResult("Discontinuity epoch", session.discontinuityEpoch ?? "—");
43
+ printResult("Build", session.buildId ?? "—");
44
+ printResult("Expires", formatDate(session.expiresAt));
45
+ // Why it stopped is the reason an operator opened this at all.
46
+ if (session.failure) {
47
+ printResult("Failure", session.failure.code);
48
+ printResult("Reason", session.failure.reason);
49
+ for (const row of session.failure.rows ?? []) {
50
+ printResult(" Row", typeof row === "string" ? row : `${row.model} ${row.id}`);
51
+ }
52
+ }
53
+ }
13
54
  /**
14
55
  * Report a document's format (#2816) — but only when it is a large document.
15
56
  *
@@ -900,6 +941,195 @@ Permissions (enforced by the server, not the CLI):
900
941
  process.exit(1);
901
942
  }
902
943
  });
944
+ // The action verb (#3435, criterion 9): a directory of records becomes a
945
+ // chunked, digested, uploaded artifact and one atomic swap of the
946
+ // document's tables. `documents ingests list|get` below is how an operator
947
+ // watches one afterwards.
948
+ //
949
+ // Destructive by nature — a bulk load deletes and overwrites records in a
950
+ // live document with no undo — so it confirms unless `-y`, which is what
951
+ // `docs/cli-design.md` reserves the destructive default for.
952
+ documents
953
+ .command("ingest")
954
+ .description("Bulk-load records into a large document from a directory")
955
+ .argument("<document-id>", "Document ID")
956
+ .requiredOption("--input <dir>", "Directory of <model>.ndjson[.gz] files, or a `documents export` output")
957
+ .option("--app <app-id>", "App ID")
958
+ .option("-y, --yes", "Skip the confirmation prompt")
959
+ .option("--no-wait", "Exit once the session is committed")
960
+ .option("--timeout <seconds>", "Give up waiting after this many seconds")
961
+ .option("--json", "Output as JSON")
962
+ .action(async (documentId, options) => {
963
+ const resolvedAppId = resolveAppId(undefined, options);
964
+ const client = new ApiClient();
965
+ let plan = null;
966
+ // The exit code is carried out of here rather than taken inside: a
967
+ // `process.exit` does not unwind, so ending the process in the try block
968
+ // would skip the cleanup in the finally and leave the compressed
969
+ // artifact — up to the whole input, gzipped — in the OS temp directory
970
+ // on every run, successful or not (finding 3435-FROZEN-05).
971
+ const run = async () => {
972
+ // The preflight, in this order and before a byte is read from disk:
973
+ // the schema read says whether this caller may see the document's
974
+ // declarations at all and hands over the ones the lines are checked
975
+ // against, and the session reader says whether this is a large
976
+ // document. Both are READS on the arm the ingest routes admit, so a
977
+ // caller who would be refused halfway is refused at once, and neither
978
+ // opens a session.
979
+ const introspection = await client.getDocumentSchema(resolvedAppId, documentId);
980
+ await client.listDocumentIngests(resolvedAppId, documentId, { limit: 1 });
981
+ const schema = ingestSchemaFromIntrospection(introspection);
982
+ const input = discoverIngestInput(options.input, documentId);
983
+ for (const ignored of input.ignored) {
984
+ info(`Ignoring ${ignored}: a bulk load reads the snapshot only.`);
985
+ }
986
+ plan = await buildIngestArtifact({ input, schema });
987
+ for (const line of summariseIngestPlan(plan))
988
+ info(line);
989
+ assertIngestConfirmable({
990
+ yes: Boolean(options.yes),
991
+ isTTY: Boolean(process.stdin.isTTY),
992
+ });
993
+ if (!options.yes) {
994
+ const confirmed = await confirmPrompt(`Bulk-load ${plan.rows} records into ${documentId}? ` +
995
+ "This replaces records in a live document and cannot be undone.");
996
+ if (!confirmed) {
997
+ info("Nothing was uploaded.");
998
+ return 1;
999
+ }
1000
+ }
1001
+ // Ctrl-C is a decision the driver makes differently depending on
1002
+ // where it lands, so it is handed the signal rather than killing the
1003
+ // process: mid-upload the session is aborted, mid-poll it is left
1004
+ // running, because by then the document is already being changed.
1005
+ const interrupt = new AbortController();
1006
+ const onSignal = () => interrupt.abort();
1007
+ process.on("SIGINT", onSignal);
1008
+ let outcome;
1009
+ try {
1010
+ outcome = await driveDocumentIngest({
1011
+ client: {
1012
+ createIngest: (id) => client.createDocumentIngest(resolvedAppId, id),
1013
+ uploadIngestChunk: (id, sessionId, chunk) => client.uploadDocumentIngestChunk(resolvedAppId, id, sessionId, chunk),
1014
+ commitIngest: (id, sessionId) => client.commitDocumentIngest(resolvedAppId, id, sessionId),
1015
+ abortIngest: (id, sessionId) => client.abortDocumentIngest(resolvedAppId, id, sessionId),
1016
+ getIngest: (id, sessionId) => client.getDocumentIngest(resolvedAppId, id, sessionId),
1017
+ },
1018
+ documentId,
1019
+ plan,
1020
+ wait: options.wait !== false,
1021
+ timeoutMs: options.timeout ? Number(options.timeout) * 1000 : null,
1022
+ signal: interrupt.signal,
1023
+ onProgress: (line) => info(line),
1024
+ });
1025
+ }
1026
+ finally {
1027
+ process.off("SIGINT", onSignal);
1028
+ }
1029
+ if (outcome.message)
1030
+ error(outcome.message);
1031
+ if (options.json) {
1032
+ json(outcome.session ?? { sessionId: outcome.sessionId });
1033
+ }
1034
+ else if (outcome.session) {
1035
+ printIngestSession(outcome.session);
1036
+ }
1037
+ else if (outcome.sessionId) {
1038
+ printResult("Session", outcome.sessionId);
1039
+ }
1040
+ return outcome.exitCode;
1041
+ };
1042
+ let code = 1;
1043
+ try {
1044
+ code = await run();
1045
+ }
1046
+ catch (err) {
1047
+ error(err.message);
1048
+ code = 1;
1049
+ }
1050
+ finally {
1051
+ await plan?.cleanup().catch(() => undefined);
1052
+ }
1053
+ // The result has to reach the pipe before the process ends: stdout to a
1054
+ // pipe is asynchronous and `process.exit` does not wait for it.
1055
+ await flushOutput();
1056
+ process.exit(code);
1057
+ });
1058
+ // Bulk-load sessions of a large document (#3434, criterion 9): a data noun
1059
+ // with `list` and `get` readers, per docs/cli-design.md, beside `snapshots`
1060
+ // below. The ACTION verb `documents ingest` is #3435's; these two are how an
1061
+ // operator sees what a session did and why one stopped.
1062
+ const ingests = documents
1063
+ .command("ingests")
1064
+ .description("Inspect a large document's bulk-load sessions");
1065
+ ingests
1066
+ .command("list")
1067
+ .description("List a large document's bulk-load sessions, newest first")
1068
+ .argument("<document-id>", "Document ID")
1069
+ .option("--app <app-id>", "App ID")
1070
+ .option("--limit <n>", "Page size (1–100)", "20")
1071
+ .option("--cursor <cursor>", "Continue from a previous page's nextCursor")
1072
+ .option("--json", "Output as JSON")
1073
+ .action(async (documentId, options) => {
1074
+ const resolvedAppId = resolveAppId(undefined, options);
1075
+ const client = new ApiClient();
1076
+ try {
1077
+ const page = await client.listDocumentIngests(resolvedAppId, documentId, {
1078
+ limit: Number(options.limit),
1079
+ ...(options.cursor ? { cursor: options.cursor } : {}),
1080
+ });
1081
+ if (options.json) {
1082
+ json(page);
1083
+ return;
1084
+ }
1085
+ if (page.items.length === 0) {
1086
+ info("No ingest sessions yet.");
1087
+ return;
1088
+ }
1089
+ console.log(formatTable(page.items, [
1090
+ { header: "SESSION", key: "sessionId" },
1091
+ { header: "STATE", key: "state" },
1092
+ { header: "CREATED", key: "createdAt", format: formatDate },
1093
+ { header: "CHUNKS", key: "chunks" },
1094
+ { header: "ROWS", key: "rows" },
1095
+ {
1096
+ header: "COMPLETED",
1097
+ key: "completedAt",
1098
+ format: (at) => (at ? formatDate(at) : "—"),
1099
+ },
1100
+ ]));
1101
+ if (page.hasMore && page.nextCursor) {
1102
+ info(`More sessions: --cursor ${page.nextCursor}`);
1103
+ }
1104
+ }
1105
+ catch (err) {
1106
+ error(err.message);
1107
+ process.exit(1);
1108
+ }
1109
+ });
1110
+ ingests
1111
+ .command("get")
1112
+ .description("Show one bulk-load session, its progress and why it stopped")
1113
+ .argument("<document-id>", "Document ID")
1114
+ .argument("<session-id>", "Session ID")
1115
+ .option("--app <app-id>", "App ID")
1116
+ .option("--json", "Output as JSON")
1117
+ .action(async (documentId, sessionId, options) => {
1118
+ const resolvedAppId = resolveAppId(undefined, options);
1119
+ const client = new ApiClient();
1120
+ try {
1121
+ const session = await client.getDocumentIngest(resolvedAppId, documentId, sessionId);
1122
+ if (options.json) {
1123
+ json(session);
1124
+ return;
1125
+ }
1126
+ printIngestSession(session);
1127
+ }
1128
+ catch (err) {
1129
+ error(err.message);
1130
+ process.exit(1);
1131
+ }
1132
+ });
903
1133
  // Snapshot builds of a large document (#3432, criterion 9): a data noun
904
1134
  // with `list` and `get` readers, per docs/cli-design.md, over the app API's
905
1135
  // read-only inspection routes.
@@ -974,47 +1204,11 @@ Permissions (enforced by the server, not the CLI):
974
1204
  json(build);
975
1205
  return;
976
1206
  }
977
- printResult("Build", build.buildId);
978
- printResult("Epoch", build.epoch);
979
- printResult("State", build.state);
980
- printResult("Source", build.source);
981
- printResult("Manifest version", build.manifestVersion);
982
- printResult("Started", formatDate(build.startedAt));
983
- printResult("Completed", build.completedAt ? formatDate(build.completedAt) : "—");
984
- printResult("Rows", build.rows);
985
- printResult("Bytes", build.bytes);
986
- printResult("Chunks", build.chunks);
987
- const verification = build.verification;
988
- printResult("Verification", verification ? verification.state : "not started");
989
- if (verification) {
990
- printResult("Checked at", formatDate(verification.checkedAt));
991
- printResult("Chunks checked", verification.chunksChecked);
992
- if (verification.code)
993
- printResult("Code", verification.code);
994
- if (verification.reason)
995
- printResult("Reason", verification.reason);
996
- }
997
- if (build.failure) {
998
- printResult("Failed at", formatDate(build.failure.at));
999
- if (build.failure.chunk) {
1000
- printResult("Failed on", `${build.failure.chunk.model} chunk ${build.failure.chunk.ordinal}`);
1001
- }
1002
- }
1003
- const timings = build.timings ?? { ticks: 0, totals: {} };
1004
- printResult("Ticks", timings.ticks);
1005
- const stages = [
1006
- ["Read (ms)", "readMs"],
1007
- ["Encode (ms)", "encodeMs"],
1008
- ["Compress (ms)", "compressMs"],
1009
- ["Hash (ms)", "hashMs"],
1010
- ["Upload (ms)", "uploadMs"],
1011
- ["Manifest (ms)", "manifestMs"],
1012
- ["Verify read (ms)", "verifyReadMs"],
1013
- ["Verify digest (ms)", "verifyDigestMs"],
1014
- ["Verify coverage (ms)", "verifyCoverageMs"],
1015
- ];
1016
- for (const [label, key] of stages) {
1017
- printResult(label, timings.totals?.[key] ?? 0);
1207
+ // The rows are composed by a pure function (#3435), so what a
1208
+ // terminal shows is checkable without spawning one; the command still
1209
+ // owns the printing.
1210
+ for (const [label, value] of snapshotBuildRows(build)) {
1211
+ printResult(label, value);
1018
1212
  }
1019
1213
  }
1020
1214
  catch (err) {
@@ -1022,6 +1216,74 @@ Permissions (enforced by the server, not the CLI):
1022
1216
  process.exit(1);
1023
1217
  }
1024
1218
  });
1219
+ // #3433, criterion 10 — the operator audit: `snapshot ⊕ overlays == table`,
1220
+ // recomputed on this side with the CLIENT's loader and fold and compared
1221
+ // with the authoritative table page by page. The one check that can catch a
1222
+ // DO/client fold divergence, which per-build verification runs on one side
1223
+ // of and therefore cannot see.
1224
+ snapshots
1225
+ .command("audit")
1226
+ .description("Recompute a large document's snapshot chain locally and compare it with the table")
1227
+ .argument("<document-id>", "Document ID")
1228
+ .option("--app <app-id>", "App ID")
1229
+ .option("--retries <n>", "Attempts before giving up on a document that keeps changing", "3")
1230
+ .option("--json", "Output as JSON")
1231
+ .action(async (documentId, options) => {
1232
+ const resolvedAppId = resolveAppId(undefined, options);
1233
+ const client = new ApiClient();
1234
+ const report = await auditDocumentChain(client, resolvedAppId, documentId, Number(options.retries));
1235
+ if (options.json) {
1236
+ json(report);
1237
+ }
1238
+ else if (report.verdict === "none") {
1239
+ // No verdict is not a clean bill of health, so nothing that could be
1240
+ // read as one is printed: the reason, and nothing else.
1241
+ error(`No verdict: ${report.reason}`);
1242
+ }
1243
+ else {
1244
+ printResult("Document", documentId);
1245
+ printResult("Base build", report.buildId);
1246
+ printResult("Base epoch", report.epoch);
1247
+ printResult("Attempts", report.attempts);
1248
+ printResult("Verdict", report.verdict === "match"
1249
+ ? "match — the table is the chain"
1250
+ : "DIVERGENCE");
1251
+ console.log(formatTable(report.models, [
1252
+ { header: "MODEL", key: "model" },
1253
+ { header: "COMPARED", key: "compared" },
1254
+ {
1255
+ header: "ONLY IN CHAIN",
1256
+ key: "onlyRecomputed",
1257
+ format: (ids) => (ids.length ? ids.join(", ") : "-"),
1258
+ },
1259
+ {
1260
+ header: "ONLY IN TABLE",
1261
+ key: "onlyTable",
1262
+ format: (ids) => (ids.length ? ids.join(", ") : "-"),
1263
+ },
1264
+ {
1265
+ header: "DIFFERING",
1266
+ key: "differing",
1267
+ format: (rows) => rows.length
1268
+ ? rows.map((row) => `${row.id} (${row.fields.join(", ")})`).join("; ")
1269
+ : "-",
1270
+ },
1271
+ ]));
1272
+ if (report.models.some((model) => model.truncated)) {
1273
+ info("Some lists were truncated; re-run with --json for the full report.");
1274
+ }
1275
+ if (report.verdict === "match")
1276
+ success("snapshot ⊕ overlays == table");
1277
+ }
1278
+ // The report has to reach the pipe before the process ends: stdout to a
1279
+ // pipe is asynchronous and `process.exit` does not wait for it, so a
1280
+ // large report piped into anything that is not reading as fast as this
1281
+ // writes used to arrive cut off mid-document (3433-C12).
1282
+ await flushOutput();
1283
+ // 0 match, 1 divergence, 2 no verdict — so the validation child can
1284
+ // script on "no verdict" without reading the text.
1285
+ process.exit(report.verdict === "match" ? 0 : report.verdict === "divergence" ? 1 : 2);
1286
+ });
1025
1287
  // Transfer document ownership
1026
1288
  documents
1027
1289
  .command("transfer-owner")
@@ -2360,4 +2622,33 @@ async function importRootDocument(client, appId, docDir, metadata, plan, overwri
2360
2622
  `root document ${targetRootDocId}`);
2361
2623
  }
2362
2624
  }
2625
+ /**
2626
+ * Wire the audit to the app API (#3433, criterion 10).
2627
+ *
2628
+ * Two permissions, not one (3433-C08): the chain export takes the app-admin
2629
+ * arm, and the schema and records reads it is compared against take the
2630
+ * console arm or a read grant on the document — the documents surface grants
2631
+ * no app-role bypass on a read (#3279, D3279-002). The schema is read first,
2632
+ * so a caller holding only the first is refused before anything is exported.
2633
+ */
2634
+ async function auditDocumentChain(client, appId, documentId, retries) {
2635
+ // One page's worth of records is the audit's whole working set on the
2636
+ // server side; everything else lives in the local store.
2637
+ const AUDIT_PAGE_SIZE = 200;
2638
+ const fingerprint = await createChainFingerprintReader();
2639
+ // Nothing the server answers is carried from one attempt to the next: the
2640
+ // attempt a moved document discards is exactly the one whose schema has
2641
+ // changed under it (3433-C09).
2642
+ const source = createAuditApiSource(client, {
2643
+ appId,
2644
+ documentId,
2645
+ pageSize: AUDIT_PAGE_SIZE,
2646
+ });
2647
+ return await auditSnapshotChain(source, {
2648
+ store: () => createAuditLocalStore(documentId),
2649
+ fingerprint,
2650
+ retries,
2651
+ pageSize: AUDIT_PAGE_SIZE,
2652
+ });
2653
+ }
2363
2654
  //# sourceMappingURL=documents.js.map