@aglyn/tenant-data-admin 1.0.0-beta.221 → 1.0.0-beta.222

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 (104) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/api-keys.js +3 -3
  3. package/src/lib/server/api-keys.js.map +1 -1
  4. package/src/lib/server/api-v1-resources.d.ts +44 -3
  5. package/src/lib/server/api-v1-resources.js +82 -5
  6. package/src/lib/server/api-v1-resources.js.map +1 -1
  7. package/src/lib/server/collection-live-pages.d.ts +2 -2
  8. package/src/lib/server/collection-live-pages.js +1 -1
  9. package/src/lib/server/collection-live-pages.js.map +1 -1
  10. package/src/lib/server/email-delivery-log.d.ts +17 -73
  11. package/src/lib/server/email-delivery-log.js +44 -222
  12. package/src/lib/server/email-delivery-log.js.map +1 -1
  13. package/src/lib/server/email-flow-gate.js +27 -38
  14. package/src/lib/server/email-flow-gate.js.map +1 -1
  15. package/src/lib/server/erase-person.d.ts +34 -54
  16. package/src/lib/server/erase-person.js +44 -239
  17. package/src/lib/server/erase-person.js.map +1 -1
  18. package/src/lib/server/host-visitor-records.d.ts +12 -4
  19. package/src/lib/server/host-visitor-records.js +20 -10
  20. package/src/lib/server/host-visitor-records.js.map +1 -1
  21. package/src/lib/server/hosted-page-review.js +46 -33
  22. package/src/lib/server/hosted-page-review.js.map +1 -1
  23. package/src/lib/server/link-reputation-review.d.ts +6 -2
  24. package/src/lib/server/link-reputation-review.js +61 -20
  25. package/src/lib/server/link-reputation-review.js.map +1 -1
  26. package/src/lib/server/live-page-drops.d.ts +67 -0
  27. package/src/lib/server/live-page-drops.js +57 -0
  28. package/src/lib/server/live-page-drops.js.map +1 -0
  29. package/src/lib/server/media-bandwidth-cap.d.ts +78 -0
  30. package/src/lib/server/media-bandwidth-cap.js +180 -0
  31. package/src/lib/server/media-bandwidth-cap.js.map +1 -0
  32. package/src/lib/server/media-cdn-icon.d.ts +77 -0
  33. package/src/lib/server/media-cdn-icon.js +140 -0
  34. package/src/lib/server/media-cdn-icon.js.map +1 -0
  35. package/src/lib/server/media-delivery-regeneration.d.ts +132 -0
  36. package/src/lib/server/media-delivery-regeneration.js +216 -0
  37. package/src/lib/server/media-delivery-regeneration.js.map +1 -0
  38. package/src/lib/server/media-delivery.d.ts +2 -0
  39. package/src/lib/server/media-delivery.js +2 -1
  40. package/src/lib/server/media-delivery.js.map +1 -1
  41. package/src/lib/server/media-serve-count.d.ts +50 -0
  42. package/src/lib/server/media-serve-count.js +62 -0
  43. package/src/lib/server/media-serve-count.js.map +1 -0
  44. package/src/lib/server/media-signing.d.ts +13 -2
  45. package/src/lib/server/media-signing.js +12 -8
  46. package/src/lib/server/media-signing.js.map +1 -1
  47. package/src/lib/server/media-tombstone.js +7 -5
  48. package/src/lib/server/media-tombstone.js.map +1 -1
  49. package/src/lib/server/media-variants.d.ts +151 -20
  50. package/src/lib/server/media-variants.js +204 -27
  51. package/src/lib/server/media-variants.js.map +1 -1
  52. package/src/lib/server/organizations.d.ts +102 -2
  53. package/src/lib/server/organizations.js +388 -58
  54. package/src/lib/server/organizations.js.map +1 -1
  55. package/src/lib/server/outbound-send-review.js +5 -4
  56. package/src/lib/server/outbound-send-review.js.map +1 -1
  57. package/src/lib/server/paged-sweeps.d.ts +51 -0
  58. package/src/lib/server/paged-sweeps.js +79 -0
  59. package/src/lib/server/paged-sweeps.js.map +1 -0
  60. package/src/lib/server/platform-marketing-consent.d.ts +0 -49
  61. package/src/lib/server/platform-marketing-consent.js +27 -11
  62. package/src/lib/server/platform-marketing-consent.js.map +1 -1
  63. package/src/lib/server/serve-media-cdn.d.ts +69 -1
  64. package/src/lib/server/serve-media-cdn.js +424 -70
  65. package/src/lib/server/serve-media-cdn.js.map +1 -1
  66. package/src/lib/server/site-icon-render.d.ts +68 -0
  67. package/src/lib/server/site-icon-render.js +157 -0
  68. package/src/lib/server/site-icon-render.js.map +1 -0
  69. package/src/lib/server/staff-seat.d.ts +50 -0
  70. package/src/lib/server/staff-seat.js +118 -0
  71. package/src/lib/server/staff-seat.js.map +1 -0
  72. package/src/lib/server/transfer-export.d.ts +46 -0
  73. package/src/lib/server/transfer-export.js +221 -0
  74. package/src/lib/server/transfer-export.js.map +1 -0
  75. package/src/lib/server/transfer-jobs.d.ts +385 -0
  76. package/src/lib/server/transfer-jobs.js +2368 -0
  77. package/src/lib/server/transfer-jobs.js.map +1 -0
  78. package/src/lib/server/transfer-packages.d.ts +127 -0
  79. package/src/lib/server/transfer-packages.js +1036 -0
  80. package/src/lib/server/transfer-packages.js.map +1 -0
  81. package/src/lib/server/upgrade-proposal.d.ts +97 -0
  82. package/src/lib/server/upgrade-proposal.js +312 -0
  83. package/src/lib/server/upgrade-proposal.js.map +1 -0
  84. package/src/lib/server/upsert-contact.d.ts +17 -8
  85. package/src/lib/server/upsert-contact.js +82 -16
  86. package/src/lib/server/upsert-contact.js.map +1 -1
  87. package/src/lib/server/web-risk.d.ts +48 -9
  88. package/src/lib/server/web-risk.js +281 -143
  89. package/src/lib/server/web-risk.js.map +1 -1
  90. package/src/tenancy.d.ts +3 -1
  91. package/src/tenancy.js +5 -1
  92. package/src/tenancy.js.map +1 -1
  93. package/src/lib/server/campaign-attribution-store.d.ts +0 -55
  94. package/src/lib/server/campaign-attribution-store.js +0 -95
  95. package/src/lib/server/campaign-attribution-store.js.map +0 -1
  96. package/src/lib/server/campaign-conversion-attribution.d.ts +0 -283
  97. package/src/lib/server/campaign-conversion-attribution.js +0 -450
  98. package/src/lib/server/campaign-conversion-attribution.js.map +0 -1
  99. package/src/lib/server/dataset-live-pages.d.ts +0 -179
  100. package/src/lib/server/dataset-live-pages.js +0 -421
  101. package/src/lib/server/dataset-live-pages.js.map +0 -1
  102. package/src/lib/server/email-revenue-attribution.d.ts +0 -185
  103. package/src/lib/server/email-revenue-attribution.js +0 -321
  104. package/src/lib/server/email-revenue-attribution.js.map +0 -1
@@ -0,0 +1,221 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ /*==========================================
19
+ * THE FIELD-SELECTABLE EXPORT (AGL-3525) — any resource, the fields the
20
+ * person chose, in their order, streamed.
21
+ *
22
+ * The resource's plugin reads its own records (`readPage`, page by page,
23
+ * holding only the chosen fields) and this module writes them as CSV, JSON
24
+ * or NDJSON (`export-file.ts` in the core), so every resource's file reads
25
+ * the same way and maps straight back in on import.
26
+ *
27
+ * ## The file says how many rows it promised
28
+ *
29
+ * A stream that dies halfway is a well-formed shorter file. So the rows are
30
+ * counted BEFORE the first byte — by the resource's `count` hook, or by
31
+ * reading ahead up to `TRANSFER_EXPORT_PREFETCH_ROWS` — and the route sends
32
+ * the count as `X-Aglyn-Export-Rows`, which the console's client checks the
33
+ * download against (`exportShortfall`'s convention). A resource that cannot
34
+ * count and holds more than the read-ahead sends no count rather than a
35
+ * wrong one.
36
+ *
37
+ * ## Scope is the plugin's to honor
38
+ *
39
+ * The export reads through the Admin SDK, past the rules, so a collaborator
40
+ * scoped to some sites is handed to `readPage` as `scopeTokens`, the tokens
41
+ * a row's `visibleTo` must hold one of — what the rules would have asked.
42
+ *
43
+ * ## Remembered choices
44
+ *
45
+ * `users/{uid}/transferPrefs/{resourceKey}` holds a person's last export
46
+ * choice and saved presets; the owner reads and writes it (rules), and the
47
+ * `fields` route reads it here to open the dialog where they left it.
48
+ *=========================================*/ import { TRANSFER_EXPORT_PAGE_ROWS, TRANSFER_EXPORT_PREFETCH_ROWS, TRANSFER_EXPORT_SELECTION_MAX, TRANSFER_PREFS_COLLECTION, normalizeTransferPrefs, resolveTransferFieldSelection, transferContentType, transferExportCsvHeader, transferExportCsvLine, transferExportFileName, transferExportHeaders, transferExportRecord } from "@aglyn/aglyn/data-transfer";
49
+ import { transferRecordsHooks, transferResourceCatalog } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
50
+ import { TransferEngineError, resolveTransferRecordsResource, transferHostIdFor } from "./transfer-jobs.js";
51
+ const FORMATS = [
52
+ 'csv',
53
+ 'json',
54
+ 'ndjson'
55
+ ];
56
+ /** The read options an export's scope makes, or the refusal. */ function readOptionsFor(input) {
57
+ const options = {
58
+ pageSize: TRANSFER_EXPORT_PAGE_ROWS
59
+ };
60
+ const scope = input.scope;
61
+ if ((scope == null ? void 0 : scope.kind) === 'selection') {
62
+ const ids = [
63
+ ...new Set((Array.isArray(scope.ids) ? scope.ids : []).filter((id)=>typeof id === 'string' && id))
64
+ ];
65
+ if (!ids.length) throw new TransferEngineError('invalid', 400, 'Select the records to export.');
66
+ if (ids.length > TRANSFER_EXPORT_SELECTION_MAX) {
67
+ throw new TransferEngineError('tooLarge', 413, `Select at most ${TRANSFER_EXPORT_SELECTION_MAX.toLocaleString('en-US')} records, or export the filter or everything.`);
68
+ }
69
+ options.ids = ids;
70
+ } else if ((scope == null ? void 0 : scope.kind) === 'filter') {
71
+ if (!scope.filter || typeof scope.filter !== 'object' || Array.isArray(scope.filter)) {
72
+ throw new TransferEngineError('invalid', 400, 'The filter could not be read.');
73
+ }
74
+ options.filter = scope.filter;
75
+ } else if ((scope == null ? void 0 : scope.kind) !== 'all') {
76
+ throw new TransferEngineError('invalid', 400, 'Say which records to export.');
77
+ }
78
+ if (input.scopeTokens) options.scopeTokens = [
79
+ ...input.scopeTokens
80
+ ];
81
+ return options;
82
+ }
83
+ /**
84
+ * The export, ready to stream (see the block header): the resource
85
+ * resolved, the fields checked against its catalog in the person's order,
86
+ * the rows counted when they can be, and a stream that reads the rest page
87
+ * by page. A field the catalog no longer has, a format the resource does not
88
+ * write, or an empty selection is refused before a byte is sent.
89
+ */ export async function streamTransferExport(deps, input) {
90
+ var _input_resource, _deps_now;
91
+ const resource = await resolveTransferRecordsResource(deps, String((_input_resource = input.resource) != null ? _input_resource : '').trim());
92
+ const options = readOptionsFor(input);
93
+ // The catalog the chosen fields are checked against is the one the dialog
94
+ // showed for the filter, for a resource whose columns follow the records read.
95
+ const ctx = _extends({
96
+ resource: resource.key,
97
+ orgId: input.orgId,
98
+ hostId: transferHostIdFor(resource, input.hostId),
99
+ actorUid: input.actorUid
100
+ }, options.filter ? {
101
+ filter: options.filter
102
+ } : {});
103
+ const format = input.format;
104
+ if (!FORMATS.includes(format) || !resource.formats.includes(format)) {
105
+ throw new TransferEngineError('unsupportedFormat', 415, `${resource.label} can be exported as ${resource.formats.join(', ').toUpperCase()}.`);
106
+ }
107
+ const catalog = await transferResourceCatalog(resource, ctx);
108
+ const selection = resolveTransferFieldSelection(catalog, (Array.isArray(input.fieldIds) ? input.fieldIds : []).filter((id)=>typeof id === 'string'));
109
+ if (selection.unknown.length) {
110
+ throw new TransferEngineError('invalid', 400, 'Some chosen fields no longer exist. Choose the fields again.', {
111
+ unknown: selection.unknown
112
+ });
113
+ }
114
+ const fieldIds = selection.fieldIds;
115
+ if (!fieldIds.length) throw new TransferEngineError('invalid', 400, 'Choose at least one field to export.');
116
+ const hooks = transferRecordsHooks(resource);
117
+ // Counted before the first byte: by the resource, or by reading ahead.
118
+ const buffered = [];
119
+ let cursor = null;
120
+ let exhausted = false;
121
+ let rows = null;
122
+ if (hooks.count) {
123
+ const { pageSize: _pageSize } = options, countOptions = _object_without_properties_loose(options, [
124
+ "pageSize"
125
+ ]);
126
+ rows = Math.max(0, Math.floor(Number(await hooks.count(ctx, countOptions)) || 0));
127
+ } else {
128
+ while(buffered.length < TRANSFER_EXPORT_PREFETCH_ROWS){
129
+ const page = await hooks.readPage(ctx, cursor, fieldIds, options);
130
+ buffered.push(...page.rows);
131
+ cursor = page.next;
132
+ if (!cursor) {
133
+ exhausted = true;
134
+ break;
135
+ }
136
+ }
137
+ if (exhausted) rows = buffered.length;
138
+ }
139
+ const encoder = new TextEncoder();
140
+ // A preset's column names (another product's layout) stand in for labels.
141
+ const headers = transferExportHeaders(fieldIds, input.headers);
142
+ const label = (fieldId)=>{
143
+ var _ref, _headers_fieldId;
144
+ var _catalog_byId_get;
145
+ return (_ref = (_headers_fieldId = headers[fieldId]) != null ? _headers_fieldId : (_catalog_byId_get = catalog.byId.get(fieldId)) == null ? void 0 : _catalog_byId_get.label) != null ? _ref : fieldId;
146
+ };
147
+ const typeOf = (fieldId)=>{
148
+ var _catalog_byId_get;
149
+ return (_catalog_byId_get = catalog.byId.get(fieldId)) == null ? void 0 : _catalog_byId_get.type;
150
+ };
151
+ let written = 0;
152
+ const write = (batch)=>{
153
+ let text = '';
154
+ for (const row of batch){
155
+ if (format === 'csv') text += `\r\n${transferExportCsvLine(row, fieldIds, typeOf)}`;
156
+ else {
157
+ const json = JSON.stringify(transferExportRecord(row, fieldIds));
158
+ text += format === 'ndjson' ? `${json}\n` : `${written ? ',' : ''}\n${json}`;
159
+ }
160
+ written += 1;
161
+ }
162
+ return text;
163
+ };
164
+ let phase = 'head';
165
+ const stream = new ReadableStream({
166
+ async pull (controller) {
167
+ try {
168
+ if (phase === 'head') {
169
+ phase = 'buffered';
170
+ const head = format === 'csv' ? `${input.bom ? '' : ''}${transferExportCsvHeader(fieldIds, label)}` : format === 'json' ? '[' : '';
171
+ if (head) {
172
+ controller.enqueue(encoder.encode(head));
173
+ return;
174
+ }
175
+ }
176
+ if (phase === 'buffered') {
177
+ phase = exhausted ? 'tail' : 'pages';
178
+ if (buffered.length) {
179
+ controller.enqueue(encoder.encode(write(buffered)));
180
+ buffered.length = 0;
181
+ return;
182
+ }
183
+ }
184
+ if (phase === 'pages') {
185
+ const page = await hooks.readPage(ctx, cursor, fieldIds, options);
186
+ cursor = page.next;
187
+ if (!cursor) phase = 'tail';
188
+ if (page.rows.length) {
189
+ controller.enqueue(encoder.encode(write(page.rows)));
190
+ return;
191
+ }
192
+ if (phase === 'pages') return;
193
+ }
194
+ controller.enqueue(encoder.encode(format === 'csv' ? '\r\n' : format === 'json' ? '\n]\n' : ''));
195
+ controller.close();
196
+ } catch (error) {
197
+ // Erroring the stream fails the download rather than completing it
198
+ // short: a half-written file the browser reports as failed.
199
+ controller.error(error);
200
+ }
201
+ }
202
+ });
203
+ return {
204
+ stream,
205
+ rows,
206
+ fileName: transferExportFileName(resource.key, format, new Date(((_deps_now = deps.now) != null ? _deps_now : Date.now)())),
207
+ contentType: transferContentType(format),
208
+ fieldIds,
209
+ label: resource.label
210
+ };
211
+ }
212
+ /** A person's remembered choices for one resource, as far as they can be trusted. */ export async function readTransferPrefs(firestore, uid, resource) {
213
+ const key = String(resource != null ? resource : '').trim();
214
+ if (!uid || !key || key.includes('/')) return {
215
+ presets: []
216
+ };
217
+ const snapshot = await firestore.collection('users').doc(uid).collection(TRANSFER_PREFS_COLLECTION).doc(key).get();
218
+ return normalizeTransferPrefs(snapshot.exists ? snapshot.data() : null);
219
+ }
220
+
221
+ //# sourceMappingURL=transfer-export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/transfer-export.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE FIELD-SELECTABLE EXPORT (AGL-3525) — any resource, the fields the\n * person chose, in their order, streamed.\n *\n * The resource's plugin reads its own records (`readPage`, page by page,\n * holding only the chosen fields) and this module writes them as CSV, JSON\n * or NDJSON (`export-file.ts` in the core), so every resource's file reads\n * the same way and maps straight back in on import.\n *\n * ## The file says how many rows it promised\n *\n * A stream that dies halfway is a well-formed shorter file. So the rows are\n * counted BEFORE the first byte — by the resource's `count` hook, or by\n * reading ahead up to `TRANSFER_EXPORT_PREFETCH_ROWS` — and the route sends\n * the count as `X-Aglyn-Export-Rows`, which the console's client checks the\n * download against (`exportShortfall`'s convention). A resource that cannot\n * count and holds more than the read-ahead sends no count rather than a\n * wrong one.\n *\n * ## Scope is the plugin's to honor\n *\n * The export reads through the Admin SDK, past the rules, so a collaborator\n * scoped to some sites is handed to `readPage` as `scopeTokens`, the tokens\n * a row's `visibleTo` must hold one of — what the rules would have asked.\n *\n * ## Remembered choices\n *\n * `users/{uid}/transferPrefs/{resourceKey}` holds a person's last export\n * choice and saved presets; the owner reads and writes it (rules), and the\n * `fields` route reads it here to open the dialog where they left it.\n *=========================================*/\n\nimport {\n TRANSFER_EXPORT_PAGE_ROWS,\n TRANSFER_EXPORT_PREFETCH_ROWS,\n TRANSFER_EXPORT_SELECTION_MAX,\n TRANSFER_PREFS_COLLECTION,\n normalizeTransferPrefs,\n resolveTransferFieldSelection,\n transferContentType,\n transferExportCsvHeader,\n transferExportCsvLine,\n transferExportFileName,\n transferExportHeaders,\n transferExportRecord,\n type TransferExportChoice,\n type TransferFormat,\n type TransferPrefs,\n} from '@aglyn/aglyn/data-transfer'\nimport {\n transferRecordsHooks,\n transferResourceCatalog,\n type TransferReadOptions,\n type TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n TransferEngineError,\n resolveTransferRecordsResource,\n transferHostIdFor,\n type TransferEngineDeps,\n} from './transfer-jobs'\n\nconst FORMATS: readonly TransferFormat[] = ['csv', 'json', 'ndjson']\n\nexport interface TransferExportInput extends TransferExportChoice {\n orgId: string\n actorUid: string\n hostId?: string | null\n /** A scoped collaborator's tokens; absent for a reader who sees the whole workspace. */\n scopeTokens?: readonly string[]\n}\n\nexport interface TransferExportFile {\n stream: ReadableStream<Uint8Array>\n /** The rows the file holds, when they could be counted before the first byte. */\n rows: number | null\n fileName: string\n contentType: string\n /** The fields written, in order. */\n fieldIds: string[]\n /** The resource's name, for the audit and the file. */\n label: string\n}\n\n/** The read options an export's scope makes, or the refusal. */\nfunction readOptionsFor(input: TransferExportInput): TransferReadOptions {\n const options: TransferReadOptions = { pageSize: TRANSFER_EXPORT_PAGE_ROWS }\n const scope = input.scope as TransferExportChoice['scope'] | undefined\n if (scope?.kind === 'selection') {\n const ids = [...new Set((Array.isArray(scope.ids) ? scope.ids : []).filter((id) => typeof id === 'string' && id))]\n if (!ids.length) throw new TransferEngineError('invalid', 400, 'Select the records to export.')\n if (ids.length > TRANSFER_EXPORT_SELECTION_MAX) {\n throw new TransferEngineError(\n 'tooLarge',\n 413,\n `Select at most ${TRANSFER_EXPORT_SELECTION_MAX.toLocaleString('en-US')} records, or export the filter or everything.`,\n )\n }\n options.ids = ids\n } else if (scope?.kind === 'filter') {\n if (!scope.filter || typeof scope.filter !== 'object' || Array.isArray(scope.filter)) {\n throw new TransferEngineError('invalid', 400, 'The filter could not be read.')\n }\n options.filter = scope.filter as Readonly<Record<string, unknown>>\n } else if (scope?.kind !== 'all') {\n throw new TransferEngineError('invalid', 400, 'Say which records to export.')\n }\n if (input.scopeTokens) options.scopeTokens = [...input.scopeTokens]\n return options\n}\n\n/**\n * The export, ready to stream (see the block header): the resource\n * resolved, the fields checked against its catalog in the person's order,\n * the rows counted when they can be, and a stream that reads the rest page\n * by page. A field the catalog no longer has, a format the resource does not\n * write, or an empty selection is refused before a byte is sent.\n */\nexport async function streamTransferExport(\n deps: Pick<TransferEngineDeps, 'resolveResource' | 'now'>,\n input: TransferExportInput,\n): Promise<TransferExportFile> {\n const resource = await resolveTransferRecordsResource(deps, String(input.resource ?? '').trim())\n const options = readOptionsFor(input)\n // The catalog the chosen fields are checked against is the one the dialog\n // showed for the filter, for a resource whose columns follow the records read.\n const ctx: TransferResourceContext = {\n resource: resource.key,\n orgId: input.orgId,\n hostId: transferHostIdFor(resource, input.hostId),\n actorUid: input.actorUid,\n ...(options.filter ? { filter: options.filter } : {}),\n }\n const format = input.format\n if (!FORMATS.includes(format) || !resource.formats.includes(format)) {\n throw new TransferEngineError(\n 'unsupportedFormat',\n 415,\n `${resource.label} can be exported as ${resource.formats.join(', ').toUpperCase()}.`,\n )\n }\n const catalog = await transferResourceCatalog(resource, ctx)\n const selection = resolveTransferFieldSelection(\n catalog,\n (Array.isArray(input.fieldIds) ? input.fieldIds : []).filter((id): id is string => typeof id === 'string'),\n )\n if (selection.unknown.length) {\n throw new TransferEngineError('invalid', 400, 'Some chosen fields no longer exist. Choose the fields again.', {\n unknown: selection.unknown,\n })\n }\n const fieldIds = selection.fieldIds\n if (!fieldIds.length) throw new TransferEngineError('invalid', 400, 'Choose at least one field to export.')\n const hooks = transferRecordsHooks(resource)\n\n // Counted before the first byte: by the resource, or by reading ahead.\n const buffered: Array<Record<string, unknown>> = []\n let cursor: string | null = null\n let exhausted = false\n let rows: number | null = null\n if (hooks.count) {\n const { pageSize: _pageSize, ...countOptions } = options\n rows = Math.max(0, Math.floor(Number(await hooks.count(ctx, countOptions)) || 0))\n } else {\n while (buffered.length < TRANSFER_EXPORT_PREFETCH_ROWS) {\n const page = await hooks.readPage(ctx, cursor, fieldIds, options)\n buffered.push(...page.rows)\n cursor = page.next\n if (!cursor) {\n exhausted = true\n break\n }\n }\n if (exhausted) rows = buffered.length\n }\n\n const encoder = new TextEncoder()\n // A preset's column names (another product's layout) stand in for labels.\n const headers = transferExportHeaders(fieldIds, input.headers)\n const label = (fieldId: string) => headers[fieldId] ?? catalog.byId.get(fieldId)?.label ?? fieldId\n const typeOf = (fieldId: string) => catalog.byId.get(fieldId)?.type\n let written = 0\n const write = (batch: ReadonlyArray<Readonly<Record<string, unknown>>>): string => {\n let text = ''\n for (const row of batch) {\n if (format === 'csv') text += `\\r\\n${transferExportCsvLine(row, fieldIds, typeOf)}`\n else {\n const json = JSON.stringify(transferExportRecord(row, fieldIds))\n text += format === 'ndjson' ? `${json}\\n` : `${written ? ',' : ''}\\n${json}`\n }\n written += 1\n }\n return text\n }\n let phase: 'head' | 'buffered' | 'pages' | 'tail' = 'head'\n const stream = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (phase === 'head') {\n phase = 'buffered'\n const head =\n format === 'csv'\n ? `${input.bom ? '' : ''}${transferExportCsvHeader(fieldIds, label)}`\n : format === 'json'\n ? '['\n : ''\n if (head) {\n controller.enqueue(encoder.encode(head))\n return\n }\n }\n if (phase === 'buffered') {\n phase = exhausted ? 'tail' : 'pages'\n if (buffered.length) {\n controller.enqueue(encoder.encode(write(buffered)))\n buffered.length = 0\n return\n }\n }\n if (phase === 'pages') {\n const page = await hooks.readPage(ctx, cursor, fieldIds, options)\n cursor = page.next\n if (!cursor) phase = 'tail'\n if (page.rows.length) {\n controller.enqueue(encoder.encode(write(page.rows)))\n return\n }\n if (phase === 'pages') return\n }\n controller.enqueue(encoder.encode(format === 'csv' ? '\\r\\n' : format === 'json' ? '\\n]\\n' : ''))\n controller.close()\n } catch (error) {\n // Erroring the stream fails the download rather than completing it\n // short: a half-written file the browser reports as failed.\n controller.error(error)\n }\n },\n })\n\n return {\n stream,\n rows,\n fileName: transferExportFileName(resource.key, format, new Date((deps.now ?? Date.now)())),\n contentType: transferContentType(format),\n fieldIds,\n label: resource.label,\n }\n}\n\n/** A person's remembered choices for one resource, as far as they can be trusted. */\nexport async function readTransferPrefs(\n firestore: FirebaseFirestore.Firestore,\n uid: string,\n resource: string,\n): Promise<TransferPrefs> {\n const key = String(resource ?? '').trim()\n if (!uid || !key || key.includes('/')) return { presets: [] }\n const snapshot = await firestore.collection('users').doc(uid).collection(TRANSFER_PREFS_COLLECTION).doc(key).get()\n return normalizeTransferPrefs(snapshot.exists ? snapshot.data() : null)\n}\n"],"names":["TRANSFER_EXPORT_PAGE_ROWS","TRANSFER_EXPORT_PREFETCH_ROWS","TRANSFER_EXPORT_SELECTION_MAX","TRANSFER_PREFS_COLLECTION","normalizeTransferPrefs","resolveTransferFieldSelection","transferContentType","transferExportCsvHeader","transferExportCsvLine","transferExportFileName","transferExportHeaders","transferExportRecord","transferRecordsHooks","transferResourceCatalog","TransferEngineError","resolveTransferRecordsResource","transferHostIdFor","FORMATS","readOptionsFor","input","options","pageSize","scope","kind","ids","Set","Array","isArray","filter","id","length","toLocaleString","scopeTokens","streamTransferExport","deps","resource","String","trim","ctx","key","orgId","hostId","actorUid","format","includes","formats","label","join","toUpperCase","catalog","selection","fieldIds","unknown","hooks","buffered","cursor","exhausted","rows","count","_pageSize","countOptions","Math","max","floor","Number","page","readPage","push","next","encoder","TextEncoder","headers","fieldId","byId","get","typeOf","type","written","write","batch","text","row","json","JSON","stringify","phase","stream","ReadableStream","pull","controller","head","bom","enqueue","encode","close","error","fileName","Date","now","contentType","readTransferPrefs","firestore","uid","presets","snapshot","collection","doc","exists","data"],"mappings":"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"}
@@ -0,0 +1,385 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type TransferJobSummary, type TransferUnfinishedImportSummary, type TransferAnalyzeResponse, type TransferCsvDelimiter, type TransferDateOrder, type TransferErrorCode, type TransferFormat, type TransferJobRecord, type TransferJobRetention, type TransferPlanChoices, type TransferPlanResponse, type TransferPlanRowsPage, type TransferProgress, type TransferResourceInfo, type TransferResultSummary, type TransferRowResult, type TransferRowVerdict, type TransferUndoCounts, type TransferUndoDecision, type TransferUndoEntry, type TransferUndoPlanResponse, type TransferUndoState, type TransferWarningClass } from '@aglyn/aglyn/data-transfer';
18
+ import { type ResolvedTransferResource, type TransferPlanRefusal, type TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
19
+ /** The one Storage object operation set the engine uses. */
20
+ export interface TransferBucketFile {
21
+ save(data: Buffer, options?: {
22
+ contentType?: string;
23
+ resumable?: boolean;
24
+ }): Promise<unknown>;
25
+ download(): Promise<[Buffer]>;
26
+ delete(options?: {
27
+ ignoreNotFound?: boolean;
28
+ }): Promise<unknown>;
29
+ }
30
+ /** The bucket the files are stored in (the Admin SDK's default bucket in production). */
31
+ export interface TransferBucket {
32
+ file(path: string): TransferBucketFile;
33
+ }
34
+ /** What the engine runs against; a spec hands it doubles. */
35
+ export interface TransferEngineDeps {
36
+ firestore: FirebaseFirestore.Firestore;
37
+ bucket: TransferBucket;
38
+ /** Defaults to `resolveTransferResource`. */
39
+ resolveResource?: (key: string) => Promise<ResolvedTransferResource>;
40
+ /** Defaults to `Date.now`. */
41
+ now?: () => number;
42
+ /** Defaults to `createResourceUid()`, the id every console resource carries. */
43
+ newJobId?: () => string;
44
+ }
45
+ /** A refusal a route answers as `{ error, code, details }` with `status`. */
46
+ export declare class TransferEngineError extends Error {
47
+ readonly code: TransferErrorCode;
48
+ readonly status: number;
49
+ readonly details?: unknown;
50
+ constructor(code: TransferErrorCode, status: number, message: string, details?: unknown);
51
+ }
52
+ /**
53
+ * A refusal for the workspace's plan (AGL-3555): a route answers its
54
+ * `refusal` as it stands — the owning plugin's flat 403 body, with
55
+ * `reason: 'plan_required'` and the plan feature as `code` — rather than
56
+ * as `{ error, code, details }`. Its own `code` is `planRequired`, what a
57
+ * client reads that body as.
58
+ */
59
+ export declare class TransferPlanRefusedError extends TransferEngineError {
60
+ readonly refusal: TransferPlanRefusal;
61
+ constructor(refusal: TransferPlanRefusal);
62
+ }
63
+ /** The collection an organization's jobs live in. */
64
+ export declare const TRANSFER_JOBS_COLLECTION = "transferJobs";
65
+ /** How long a driver's lease outlives its budget, so a crashed request's job is resumed soon after. */
66
+ export declare const TRANSFER_LEASE_GRACE_MS = 30000;
67
+ /** How long an `applying` job sits untouched before the sweep resumes it. */
68
+ export declare const TRANSFER_STALE_MS: number;
69
+ /** Below this much budget, a request stops rather than starting another chunk. */
70
+ export declare const TRANSFER_MIN_CHUNK_BUDGET_MS = 2000;
71
+ /** Where an organization's jobs are. */
72
+ export declare function transferJobsCollection(firestore: FirebaseFirestore.Firestore, orgId: string): FirebaseFirestore.CollectionReference;
73
+ /** Where a job's file is stored. */
74
+ export declare function transferSourcePath(orgId: string, jobId: string): string;
75
+ /** Where one part of a file sent in parts waits until the rest arrives. */
76
+ export declare function transferPartPath(orgId: string, jobId: string, part: number): string;
77
+ export declare const clock: (deps: Pick<TransferEngineDeps, "now">) => number;
78
+ /** A value as Firestore can store it: no `undefined`, no class instances. */
79
+ export declare function stored<T>(value: T): T;
80
+ export declare function emptyResults(): TransferResultSummary;
81
+ export declare function emptyUndoCounts(): TransferUndoCounts;
82
+ /** `fn` over `items`, at most `limit` at once. */
83
+ export declare function eachLimited<T>(items: readonly T[], limit: number, fn: (item: T) => Promise<void>): Promise<void>;
84
+ export declare function isAlreadyExists(error: unknown): boolean;
85
+ /**
86
+ * Writes `value` as JSON on `ref` beside `fields`. JSON, because a plan row
87
+ * holds whatever the file held — a nested list from a JSON file is a value
88
+ * Firestore refuses — and past {@link JSON_PIECE_CHARS} the text goes into
89
+ * `pieces/{k}` documents, so a chunk of long notes is never refused for size.
90
+ */
91
+ export declare function writeJsonDoc(ref: FirebaseFirestore.DocumentReference, fields: Record<string, unknown>, value: unknown): Promise<void>;
92
+ /** The value {@link writeJsonDoc} wrote, or `null` when the document is absent. */
93
+ export declare function readJsonDoc<T>(ref: FirebaseFirestore.DocumentReference): Promise<T | null>;
94
+ /** Deletes every document of a collection (and a JSON document's pieces), eight at a time. */
95
+ export declare function clearCollection(collection: FirebaseFirestore.CollectionReference): Promise<void>;
96
+ export declare function readJob(deps: TransferEngineDeps, orgId: string, jobId: string): Promise<TransferJobRecord>;
97
+ /**
98
+ * When the sweep may clean a job up, and how (see the sweep below): never
99
+ * while it runs or undoes, nor once trimmed; a job that never wrote expires
100
+ * {@link TRANSFER_DRAFT_RETENTION_MS} after it was last touched; one that
101
+ * wrote is trimmed once its undo window has closed.
102
+ */
103
+ export declare function transferJobRetention(job: TransferJobRecord): {
104
+ retention: TransferJobRetention;
105
+ retainUntil: number;
106
+ } | null;
107
+ export declare function saveJob(deps: TransferEngineDeps, job: TransferJobRecord): Promise<void>;
108
+ /** The job as a route returns it. */
109
+ export declare function readTransferJob(deps: TransferEngineDeps, input: {
110
+ orgId: string;
111
+ jobId: string;
112
+ }): Promise<TransferJobRecord>;
113
+ /** A `records` resource, joined to its server half, or the refusal a route answers. */
114
+ export declare function resolveTransferRecordsResource(deps: Pick<TransferEngineDeps, 'resolveResource'>, key: string): Promise<ResolvedTransferResource>;
115
+ /**
116
+ * The site a transfer concerns: required for a `host` resource; for an
117
+ * `org` resource the site the person named, or `null` — a workspace's
118
+ * records can be read through one site's view of them and imported as
119
+ * that site's captures (the CRM's, AGL-3541), and the gate has already
120
+ * checked `data.manage` on that site.
121
+ */
122
+ export declare function transferHostIdFor(resource: ResolvedTransferResource, hostId: string | null | undefined): string | null;
123
+ export declare function contextFor(job: TransferJobRecord, actorUid: string | null): TransferResourceContext;
124
+ export declare function moveJob(job: TransferJobRecord, to: TransferJobRecord['status'], now: number, patch?: Partial<TransferJobRecord>): TransferJobRecord;
125
+ /** Where a job stands, for a progress bar. */
126
+ export declare function transferJobProgress(job: TransferJobRecord): TransferProgress;
127
+ /**
128
+ * What a resource offers the wizard and the export dialog: its descriptor,
129
+ * every field and group, its match keys (and the ones a person starts with,
130
+ * which the Re-importable preset leads with), the presets' hints and its own
131
+ * presets, its locked rules and aliases. `prefs` is the person's own
132
+ * and is filled by the caller; a custom field cannot be created from here.
133
+ */
134
+ export declare function readTransferResourceInfo(deps: TransferEngineDeps, input: {
135
+ orgId: string;
136
+ actorUid: string;
137
+ resource: string;
138
+ hostId?: string | null;
139
+ filter?: unknown;
140
+ }): Promise<Omit<TransferResourceInfo, 'prefs'>>;
141
+ export interface TransferUploadInput {
142
+ orgId: string;
143
+ actorUid: string;
144
+ resource: string;
145
+ hostId?: string | null;
146
+ fileName: string;
147
+ format?: TransferFormat;
148
+ content: string;
149
+ part?: number;
150
+ parts?: number;
151
+ jobId?: string;
152
+ delimiter?: TransferCsvDelimiter;
153
+ headerRow?: boolean;
154
+ }
155
+ /**
156
+ * Stores a file, or one part of it, and makes the job on the first part.
157
+ * Each part is inspected before it is stored; the whole file is inspected
158
+ * again, read, counted against the resource's limits and stored as the
159
+ * source once the last part lands. A part sent twice is accepted once.
160
+ */
161
+ export declare function uploadTransferSource(deps: TransferEngineDeps, input: TransferUploadInput): Promise<{
162
+ job: TransferJobRecord;
163
+ complete: boolean;
164
+ }>;
165
+ export interface AnalyzeTransferInput {
166
+ orgId: string;
167
+ jobId: string;
168
+ actorUid: string;
169
+ mapping?: Record<number, string | null>;
170
+ /** Match key field ids, in priority order; the resource's defaults when absent. */
171
+ matchKeys?: string[];
172
+ dateOrders?: Record<string, TransferDateOrder>;
173
+ }
174
+ /**
175
+ * Reads the file and proposes a mapping: every column against the
176
+ * resource's catalog and aliases, with the first rows as samples, and each
177
+ * mapped picklist column's values against the organization's list. With
178
+ * `mapping`, the values are read under that mapping instead of the proposal,
179
+ * and the answer adds what reading every mapped field did over the whole
180
+ * file and how the rows match existing records under `matchKeys`.
181
+ */
182
+ export declare function analyzeTransferJob(deps: TransferEngineDeps, input: AnalyzeTransferInput): Promise<Omit<TransferAnalyzeResponse, 'ok'>>;
183
+ /**
184
+ * The dry run: every row read, its picklist values resolved, matched to the
185
+ * records that exist and planned under the policy — without writing a
186
+ * record. The planned rows are stored chunk by chunk for Apply; the summary,
187
+ * the warnings and the first page answer the wizard.
188
+ */
189
+ export declare function planTransferJob(deps: TransferEngineDeps, input: {
190
+ orgId: string;
191
+ jobId: string;
192
+ actorUid: string;
193
+ choices: TransferPlanChoices;
194
+ }): Promise<Omit<TransferPlanResponse, 'ok'>>;
195
+ /** One page of the stored dry run, optionally only rows with some verdicts. */
196
+ export declare function readTransferPlanRows(deps: TransferEngineDeps, input: {
197
+ orgId: string;
198
+ jobId: string;
199
+ offset?: number;
200
+ limit?: number;
201
+ verdicts?: readonly TransferRowVerdict[];
202
+ }): Promise<TransferPlanRowsPage>;
203
+ export interface ApplyTransferInput {
204
+ orgId: string;
205
+ jobId: string;
206
+ /** Whose import it is, for the plugin's own checks and activity lines. */
207
+ actorUid: string;
208
+ acknowledged?: readonly TransferWarningClass[];
209
+ /** When this request must stop starting chunks (epoch ms). */
210
+ deadlineMs: number;
211
+ /** Who is driving: a request id, or the sweep. */
212
+ driver: string;
213
+ }
214
+ export interface ApplyTransferOutcome {
215
+ job: TransferJobRecord;
216
+ progress: TransferProgress;
217
+ done: boolean;
218
+ /** This call moved the job into `applying` (the first call, or a resume after a failure). */
219
+ started: boolean;
220
+ /** That move was a resume after a failure. */
221
+ resumed: boolean;
222
+ /** The results of the chunks this call wrote. */
223
+ results: TransferRowResult[];
224
+ }
225
+ export declare function addResults(total: TransferResultSummary, more: TransferResultSummary): TransferResultSummary;
226
+ export interface LedgerEntry {
227
+ result: TransferRowResult;
228
+ undo: TransferUndoEntry | null;
229
+ }
230
+ /** Takes the job for `driver`, or refuses: another driver holds it, or another import of the resource is running. */
231
+ export declare function takeLease(deps: TransferEngineDeps, job: TransferJobRecord, driver: string, expiresAt: number, enter: (current: TransferJobRecord, now: number) => TransferJobRecord): Promise<TransferJobRecord>;
232
+ /**
233
+ * Writes chunks until the budget runs out (see the block header). The first
234
+ * call needs every required acknowledgement; later calls — the browser's
235
+ * next, or the sweep's — resume at the cursor. A plugin `apply` that throws
236
+ * fails the job with the chunk named, and calling again resumes it.
237
+ */
238
+ export declare function applyTransferJob(deps: TransferEngineDeps, input: ApplyTransferInput): Promise<ApplyTransferOutcome>;
239
+ /** The job, its progress, and whether undo is open. */
240
+ export declare function readTransferJobStatus(deps: TransferEngineDeps, input: {
241
+ orgId: string;
242
+ jobId: string;
243
+ include?: 'results';
244
+ }): Promise<{
245
+ job: TransferJobRecord;
246
+ progress: TransferProgress;
247
+ undo: {
248
+ available: boolean;
249
+ expiresAt: number | null;
250
+ state: TransferUndoState | null;
251
+ };
252
+ rows?: TransferRowResult[];
253
+ }>;
254
+ /**
255
+ * The result file: every row of the file as it was sent, then what happened
256
+ * to it, why, and the record it wrote. A row not written yet says `pending`.
257
+ */
258
+ export declare function transferResultFile(deps: TransferEngineDeps, input: {
259
+ orgId: string;
260
+ jobId: string;
261
+ }): Promise<{
262
+ csv: string;
263
+ rows: number;
264
+ fileName: string;
265
+ }>;
266
+ /**
267
+ * The workspace's imports, newest first, a page at a time, as the hub's
268
+ * history lists them. Clients cannot read the jobs' subcollections, and
269
+ * the hub needs more than the job document says — whether undo is still
270
+ * open, whether the result file can still be made — so the route answers
271
+ * this. `labels` names each resource; `emailsOf` says who each member is.
272
+ */
273
+ export declare function listTransferJobs(deps: TransferEngineDeps, input: {
274
+ orgId: string;
275
+ limit?: number;
276
+ after?: number | null;
277
+ labels?: Readonly<Record<string, string>>;
278
+ emailsOf?: (uids: readonly string[]) => Promise<ReadonlyMap<string, string>>;
279
+ }): Promise<{
280
+ jobs: TransferJobSummary[];
281
+ next: number | null;
282
+ }>;
283
+ /**
284
+ * The caller's unfinished imports of records, newest first — a Firestore
285
+ * query on `(createdBy, retention, updatedAt ↓)`, never a page of the
286
+ * history filtered.
287
+ */
288
+ export declare function listUnfinishedTransferJobs(deps: TransferEngineDeps, input: {
289
+ orgId: string;
290
+ actorUid: string;
291
+ labels?: Readonly<Record<string, string>>;
292
+ }): Promise<TransferUnfinishedImportSummary[]>;
293
+ /**
294
+ * Throws one of the caller's unfinished imports away now: its job, its
295
+ * pieces and its file, as the cleanup would a week on. Refused for someone
296
+ * else's import, and for one that has written anything — that is undone.
297
+ */
298
+ export declare function discardTransferJob(deps: TransferEngineDeps, input: {
299
+ orgId: string;
300
+ jobId: string;
301
+ actorUid: string;
302
+ }): Promise<void>;
303
+ /**
304
+ * What undo would do, writing nothing: every record the import touched,
305
+ * read as it is now, and planned with the core's `planTransferUndo`. A
306
+ * record edited since comes back as a conflict, with what it holds now and
307
+ * what undo would put back, for the person to decide.
308
+ */
309
+ export declare function planTransferJobUndo(deps: TransferEngineDeps, input: {
310
+ orgId: string;
311
+ jobId: string;
312
+ actorUid: string;
313
+ offset?: number;
314
+ limit?: number;
315
+ }): Promise<Omit<TransferUndoPlanResponse, 'ok'>>;
316
+ export interface ApplyTransferUndoInput {
317
+ orgId: string;
318
+ jobId: string;
319
+ actorUid: string;
320
+ decisions?: Readonly<Record<string, TransferUndoDecision>>;
321
+ otherwise: TransferUndoDecision;
322
+ deadlineMs: number;
323
+ driver: string;
324
+ }
325
+ /**
326
+ * Carries undo out, chunk by chunk, through the plugin's `revert`, each
327
+ * record with the person's decision (`otherwise` for one the decisions do
328
+ * not name). Called again until `done`; the last chunk moves the job to
329
+ * `undone`. Reverting a chunk twice is harmless: a field already restored
330
+ * plans as nothing.
331
+ */
332
+ export declare function applyTransferJobUndo(deps: TransferEngineDeps, input: ApplyTransferUndoInput): Promise<{
333
+ job: TransferJobRecord;
334
+ undo: TransferUndoState;
335
+ done: boolean;
336
+ started: boolean;
337
+ }>;
338
+ export interface TransferSweepOutcome {
339
+ /** Jobs found applying and untouched past the stale window. */
340
+ found: number;
341
+ resumed: Array<{
342
+ orgId: string;
343
+ jobId: string;
344
+ status: TransferJobRecord['status'];
345
+ done: boolean;
346
+ }>;
347
+ /** Jobs that could not be resumed this time (held by a driver, or failed). */
348
+ skipped: Array<{
349
+ orgId: string;
350
+ jobId: string;
351
+ reason: string;
352
+ }>;
353
+ /** Undos found running and untouched, and what resuming each did. */
354
+ undone: Array<{
355
+ orgId: string;
356
+ jobId: string;
357
+ done: boolean;
358
+ }>;
359
+ /** Jobs that never wrote, deleted with their file. */
360
+ expired: Array<{
361
+ orgId: string;
362
+ jobId: string;
363
+ }>;
364
+ /** Jobs past their undo window, cleared to the job and its results. */
365
+ trimmed: Array<{
366
+ orgId: string;
367
+ jobId: string;
368
+ }>;
369
+ }
370
+ /** A job that never wrote, gone: its subcollections, its file, then the job. */
371
+ export declare function expireTransferJob(deps: TransferEngineDeps, job: TransferJobRecord): Promise<void>;
372
+ /** A job past its undo window, kept as the job and its results: the dry run, undo snapshots, ledger and file cleared. */
373
+ export declare function trimTransferJob(deps: TransferEngineDeps, job: TransferJobRecord): Promise<TransferJobRecord>;
374
+ /**
375
+ * The sweep (see the block header). Each resume runs under the sweep's
376
+ * lease until the budget is spent; the ledger makes overlapping with a
377
+ * returning browser harmless. `dryRun` lists what it would do and does
378
+ * nothing.
379
+ */
380
+ export declare function sweepAbandonedTransferJobs(deps: TransferEngineDeps, input: {
381
+ deadlineMs: number;
382
+ dryRun?: boolean;
383
+ limit?: number;
384
+ staleMs?: number;
385
+ }): Promise<TransferSweepOutcome>;