@rebasepro/server 0.25.1 → 0.26.0

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 (157) hide show
  1. package/dist/{GCSStorageController-CRd9bFrc.js → GCSStorageController-40FyFeSx.js} +6 -2
  2. package/dist/GCSStorageController-40FyFeSx.js.map +1 -0
  3. package/dist/{S3StorageController-D00EFa5P.js → S3StorageController-Bl5shp94.js} +6 -2
  4. package/dist/S3StorageController-Bl5shp94.js.map +1 -0
  5. package/dist/access-BFAUvsSs.js +1897 -0
  6. package/dist/access-BFAUvsSs.js.map +1 -0
  7. package/dist/api/branch-routes.d.ts +25 -0
  8. package/dist/api/live-schema-routes.d.ts +10 -0
  9. package/dist/api/rest/api-generator.d.ts +9 -1
  10. package/dist/api/rest/idempotency.d.ts +15 -1
  11. package/dist/api/root-error-handler.d.ts +6 -5
  12. package/dist/api/types.d.ts +12 -0
  13. package/dist/{ast-schema-editor-mCeSC03x.js → ast-schema-editor-madTa9Hh.js} +2 -2
  14. package/dist/{ast-schema-editor-mCeSC03x.js.map → ast-schema-editor-madTa9Hh.js.map} +1 -1
  15. package/dist/auth/access.d.ts +10 -0
  16. package/dist/auth/adapter-middleware.d.ts +2 -1
  17. package/dist/auth/adapter-service-key.d.ts +24 -0
  18. package/dist/auth/api-keys/api-key-middleware.d.ts +21 -1
  19. package/dist/auth/api-keys/api-key-store.d.ts +8 -1
  20. package/dist/auth/api-keys/key-grant.d.ts +18 -1
  21. package/dist/auth/expired-token-sweep.d.ts +5 -0
  22. package/dist/auth/impersonation.d.ts +3 -1
  23. package/dist/auth/jwt.d.ts +8 -2
  24. package/dist/auth/oauth-signin-policy.d.ts +21 -35
  25. package/dist/auth/rls-scope.d.ts +17 -7
  26. package/dist/auth/rls-simulator-routes.d.ts +51 -0
  27. package/dist/auth/simulation-read-only.d.ts +28 -0
  28. package/dist/auth/simulation.d.ts +141 -0
  29. package/dist/auth/sql-rate-limit-store.d.ts +15 -1
  30. package/dist/{auth-Dkvo2tTJ.js → auth-izI7AEzC.js} +370 -1880
  31. package/dist/auth-izI7AEzC.js.map +1 -0
  32. package/dist/auth_adapter-B7RUMk6e.js +89 -0
  33. package/dist/auth_adapter-B7RUMk6e.js.map +1 -0
  34. package/dist/backend/create-backend.d.ts +6 -0
  35. package/dist/backend/options.d.ts +9 -4
  36. package/dist/{backend-DTAOsLQc.js → backend-CaG_Jyso.js} +9 -2
  37. package/dist/backend-CaG_Jyso.js.map +1 -0
  38. package/dist/{backup-B9m3cb3X.js → backup-BVbHoKo8.js} +6 -6
  39. package/dist/{backup-B9m3cb3X.js.map → backup-BVbHoKo8.js.map} +1 -1
  40. package/dist/boot/boot.d.ts +2 -15
  41. package/dist/boot/ddl-bootstrap.d.ts +34 -0
  42. package/dist/boot/env.d.ts +6 -0
  43. package/dist/boot/fetch-bundle.d.ts +5 -1
  44. package/dist/boot/provision-only.d.ts +23 -0
  45. package/dist/boot/provision.d.ts +53 -2
  46. package/dist/boot/role.d.ts +2 -0
  47. package/dist/boot/schema-change-recorder.d.ts +45 -0
  48. package/dist/boot/security-headers.d.ts +22 -0
  49. package/dist/branch-routes-B2vvEx53.js +84 -0
  50. package/dist/branch-routes-B2vvEx53.js.map +1 -0
  51. package/dist/{contract-routes-CsFXW7j4.js → contract-routes-CU2A3JnF.js} +2 -2
  52. package/dist/{contract-routes-CsFXW7j4.js.map → contract-routes-CU2A3JnF.js.map} +1 -1
  53. package/dist/cron/cron-store.d.ts +8 -1
  54. package/dist/{cron-loader-Dj6lA4HQ.js → cron-loader-mMhqbVsl.js} +2 -2
  55. package/dist/{cron-loader-Dj6lA4HQ.js.map → cron-loader-mMhqbVsl.js.map} +1 -1
  56. package/dist/{cron-routes-CwbLZguO.js → cron-routes-Cs33j_Kr.js} +3 -3
  57. package/dist/{cron-routes-CwbLZguO.js.map → cron-routes-Cs33j_Kr.js.map} +1 -1
  58. package/dist/{cron-scheduler-B04ACoUE.js → cron-scheduler-Be7CNghL.js} +2 -2
  59. package/dist/{cron-scheduler-B04ACoUE.js.map → cron-scheduler-Be7CNghL.js.map} +1 -1
  60. package/dist/{cron-store-DkyfuLOH.js → cron-store-DLn11M8H.js} +10 -9
  61. package/dist/cron-store-DLn11M8H.js.map +1 -0
  62. package/dist/{strings.es-BeZWAodn.js → data_driver-D0mg1Y_x.js} +2 -90
  63. package/dist/data_driver-D0mg1Y_x.js.map +1 -0
  64. package/dist/{ddl-bootstrap-Ck20GSIX.js → ddl-bootstrap-CjRrn12l.js} +91 -2
  65. package/dist/ddl-bootstrap-CjRrn12l.js.map +1 -0
  66. package/dist/deploy/pod-contract.d.ts +1 -1
  67. package/dist/email/dev-sink.d.ts +6 -0
  68. package/dist/email/index.d.ts +1 -1
  69. package/dist/{soft-delete-params-BmlVwecC.js → field-access-DYsvjI_P.js} +27 -57
  70. package/dist/field-access-DYsvjI_P.js.map +1 -0
  71. package/dist/{filter-dialect-O1HK6Nvr.js → filter-dialect-TrKFWLqT.js} +3 -3
  72. package/dist/{filter-dialect-O1HK6Nvr.js.map → filter-dialect-TrKFWLqT.js.map} +1 -1
  73. package/dist/{function-loader-KKDqTcIr.js → function-loader-B8YzXEsQ.js} +21 -14
  74. package/dist/function-loader-B8YzXEsQ.js.map +1 -0
  75. package/dist/functions/hono-like.d.ts +12 -0
  76. package/dist/functions/index.js +19 -1
  77. package/dist/functions/index.js.map +1 -1
  78. package/dist/functions/request-timeout.d.ts +3 -3
  79. package/dist/history/history-routes.d.ts +6 -0
  80. package/dist/history/retention-sweep.d.ts +5 -0
  81. package/dist/{history-recorder-B2rkuhuy.js → history-recorder-Bsau7h4X.js} +6 -6
  82. package/dist/history-recorder-Bsau7h4X.js.map +1 -0
  83. package/dist/{history-store-DPofGVDp.js → history-store-BQz5YCkW.js} +26 -2
  84. package/dist/history-store-BQz5YCkW.js.map +1 -0
  85. package/dist/index.d.ts +3 -1
  86. package/dist/index.es.js +2449 -543
  87. package/dist/index.es.js.map +1 -1
  88. package/dist/init/process-safety.d.ts +4 -3
  89. package/dist/init/shutdown.d.ts +8 -0
  90. package/dist/init.d.ts +6 -1
  91. package/dist/jobs/job-store.d.ts +8 -1
  92. package/dist/{jobs-h1WRq3cp.js → jobs-Co6Ni5HP.js} +6 -6
  93. package/dist/jobs-Co6Ni5HP.js.map +1 -0
  94. package/dist/{jwt-D3oFauZI.js → jwt-rDS0884_.js} +4 -3
  95. package/dist/{jwt-D3oFauZI.js.map → jwt-rDS0884_.js.map} +1 -1
  96. package/dist/{keys-COzQ2aE8.js → keys-BXcdRNhJ.js} +2 -2
  97. package/dist/{keys-COzQ2aE8.js.map → keys-BXcdRNhJ.js.map} +1 -1
  98. package/dist/mcp/consent-page.d.ts +38 -1
  99. package/dist/mcp/mcp-routes.d.ts +9 -0
  100. package/dist/mcp/mcp-tools.d.ts +7 -1
  101. package/dist/mcp/oauth-store.d.ts +12 -5
  102. package/dist/metrics/history-recorder.d.ts +4 -1
  103. package/dist/metrics/history-store.d.ts +8 -0
  104. package/dist/{offline-BrqCRgih-BtDB1ecy.js → offline-AvuEcg7u-DFHL98OT.js} +127 -40
  105. package/dist/offline-AvuEcg7u-DFHL98OT.js.map +1 -0
  106. package/dist/{offline-store-BhYXViha-BW5j1We_.js → offline-store-BhYXViha-YTmjBpo2.js} +5 -5
  107. package/dist/{offline-store-BhYXViha-BW5j1We_.js.map → offline-store-BhYXViha-YTmjBpo2.js.map} +1 -1
  108. package/dist/{openapi-generator-BjHr6upl.js → openapi-generator-CyxEtYEX.js} +61 -22
  109. package/dist/openapi-generator-CyxEtYEX.js.map +1 -0
  110. package/dist/{proxy-CH8Jsovg.js → proxy-BYir8tWK.js} +4 -6
  111. package/dist/proxy-BYir8tWK.js.map +1 -0
  112. package/dist/{query-parser-otFZqJsC.js → query-parser-CDXWf2hH.js} +6 -5
  113. package/dist/{query-parser-otFZqJsC.js.map → query-parser-CDXWf2hH.js.map} +1 -1
  114. package/dist/{request-timeout-59crS-9u.js → request-timeout-DVjo-c_8.js} +4 -4
  115. package/dist/request-timeout-DVjo-c_8.js.map +1 -0
  116. package/dist/rls-simulator-routes-bEY7-5-u.js +190 -0
  117. package/dist/rls-simulator-routes-bEY7-5-u.js.map +1 -0
  118. package/dist/schema-edit/deployed-schema-guard.d.ts +47 -0
  119. package/dist/{schema-editor-routes-Bsut0bCC.js → schema-editor-routes-Dc3DU_jm.js} +3 -3
  120. package/dist/{schema-editor-routes-Bsut0bCC.js.map → schema-editor-routes-Dc3DU_jm.js.map} +1 -1
  121. package/dist/soft-delete-params-BRONCOA2.js +59 -0
  122. package/dist/soft-delete-params-BRONCOA2.js.map +1 -0
  123. package/dist/{sql-export-routes-DmB_uZ4l.js → sql-export-routes-QycX8W8S.js} +38 -4
  124. package/dist/sql-export-routes-QycX8W8S.js.map +1 -0
  125. package/dist/{src-D5Y3N-S4.js → src-Bh9LRPb9.js} +21 -2
  126. package/dist/{src-D5Y3N-S4.js.map → src-Bh9LRPb9.js.map} +1 -1
  127. package/dist/storage/GCSStorageController.d.ts +2 -0
  128. package/dist/storage/LocalStorageController.d.ts +8 -3
  129. package/dist/storage/S3StorageController.d.ts +2 -0
  130. package/dist/storage/maintenance-keys.d.ts +17 -0
  131. package/dist/storage/maintenance-routes.d.ts +28 -5
  132. package/dist/storage/maintenance.d.ts +136 -4
  133. package/dist/storage/orphan-scans.d.ts +120 -0
  134. package/dist/storage/types.d.ts +11 -0
  135. package/dist/strings.es-B7QLnHh2.js +96 -0
  136. package/dist/strings.es-B7QLnHh2.js.map +1 -0
  137. package/dist/upload-validation-Bgo626uY.js.map +1 -1
  138. package/package.json +5 -5
  139. package/dist/GCSStorageController-CRd9bFrc.js.map +0 -1
  140. package/dist/S3StorageController-D00EFa5P.js.map +0 -1
  141. package/dist/auth-Dkvo2tTJ.js.map +0 -1
  142. package/dist/auth_adapter-CIItCTc8.js +0 -21
  143. package/dist/auth_adapter-CIItCTc8.js.map +0 -1
  144. package/dist/backend-DTAOsLQc.js.map +0 -1
  145. package/dist/cron-store-DkyfuLOH.js.map +0 -1
  146. package/dist/ddl-bootstrap-Ck20GSIX.js.map +0 -1
  147. package/dist/function-loader-KKDqTcIr.js.map +0 -1
  148. package/dist/history-recorder-B2rkuhuy.js.map +0 -1
  149. package/dist/history-store-DPofGVDp.js.map +0 -1
  150. package/dist/jobs-h1WRq3cp.js.map +0 -1
  151. package/dist/offline-BrqCRgih-BtDB1ecy.js.map +0 -1
  152. package/dist/openapi-generator-BjHr6upl.js.map +0 -1
  153. package/dist/proxy-CH8Jsovg.js.map +0 -1
  154. package/dist/request-timeout-59crS-9u.js.map +0 -1
  155. package/dist/soft-delete-params-BmlVwecC.js.map +0 -1
  156. package/dist/sql-export-routes-DmB_uZ4l.js.map +0 -1
  157. package/dist/strings.es-BeZWAodn.js.map +0 -1
@@ -24,6 +24,8 @@ export declare class GCSStorageController implements StorageController {
24
24
  * @see S3StorageController.knownBuckets
25
25
  */
26
26
  knownBuckets(): string[];
27
+ /** See {@link StorageController.storageLocation}: bucket names are global. */
28
+ storageLocation(): string;
27
29
  putObject({ file, key, metadata, bucket }: UploadFileProps, options?: PutObjectOptions): Promise<UploadFileResult>;
28
30
  /**
29
31
  * Read the object's metadata and nothing else — no signing.
@@ -67,6 +67,11 @@ export declare class LocalStorageController implements StorageController {
67
67
  * not have been written to yet.
68
68
  */
69
69
  knownBuckets(): string[];
70
+ /**
71
+ * See {@link StorageController.storageLocation}: the default bucket's
72
+ * directory, with the storage root's links resolved.
73
+ */
74
+ storageLocation(): string;
70
75
  /**
71
76
  * Ensure directory exists, creating it if necessary
72
77
  */
@@ -128,11 +133,11 @@ export declare class LocalStorageController implements StorageController {
128
133
  /**
129
134
  * A folder's entries for a listing page.
130
135
  *
131
- * A page token is an index into these entries, and every continuation
132
- * page used to read the whole folder again to find its place: a folder of
136
+ * A page token names the last of these entries a page read, and every
137
+ * continuation page used to read the whole folder again to find its place: a folder of
133
138
  * n entries listed in pages of p cost n/p full reads. A first page reads
134
139
  * the folder and, when there is a next page, keeps the read; a
135
- * continuation page indexes into it while all of these hold:
140
+ * continuation page finds its place in it while all of these hold:
136
141
  *
137
142
  * - the folder's mtime is the one read with it. Adding, removing or
138
143
  * renaming an entry changes it, which is the invalidation;
@@ -24,6 +24,8 @@ export declare class S3StorageController implements StorageController {
24
24
  * can reach — and a mistyped one came back as "file not found".
25
25
  */
26
26
  knownBuckets(): string[];
27
+ /** See {@link StorageController.storageLocation}: the endpoint (AWS when none) and the bucket. */
28
+ storageLocation(): string;
27
29
  /** See {@link StorageController.maxFileSize}. */
28
30
  maxFileSize(): number;
29
31
  /**
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Where storage maintenance keeps its own records inside a source: the state
3
+ * and result of an orphan scan (`rebase storage orphans`), which a worker
4
+ * writes and any server process reads back.
5
+ *
6
+ * Reserved at the route layer like the rendition space, in both directions. A
7
+ * scan's result is the list a later delete acts on, so a caller who could
8
+ * write one could name files for an administrator's delete to remove; and it
9
+ * lists keys a caller may not be allowed to see.
10
+ */
11
+ export declare const MAINTENANCE_PREFIX = "_rebase/maintenance/";
12
+ /**
13
+ * True for a key in the reserved maintenance space — the prefix itself, or
14
+ * anything under it. Folded the way the filesystem under a local bucket folds
15
+ * it, as {@link isRenditionKey} is.
16
+ */
17
+ export declare function isMaintenanceKey(key: string): boolean;
@@ -1,23 +1,46 @@
1
1
  /**
2
2
  * `/admin/storage` — the routes behind `rebase storage orphans` and
3
3
  * `rebase storage move`. Admin-only; the caller mounts them behind the admin
4
- * gate (`database:read` to read, `database:write` to delete or move).
4
+ * gate (`database:read` to read, `database:write` for every other method).
5
5
  *
6
- * GET /orphans?storageId=&olderThanHours= objects no row names
7
- * DELETE /orphans?storageId=&olderThanHours= delete them, found again first
6
+ * POST /orphans/scans?storageId=&olderThanHours= start a scan, in the background
7
+ * GET /orphans/scans/:id?storageId= its progress, then its report
8
+ * DELETE /orphans?scan=:id&storageId= delete a completed scan's orphans, in the background
9
+ * GET /orphans?storageId=&olderThanHours= deprecated: a scan inside the request
10
+ * DELETE /orphans?storageId=&olderThanHours= deprecated: scan and delete inside the request
8
11
  * POST /move { from, to, prefix?, cursor?, limit?, deleteSource? }
12
+ * POST /move/verify (the same body) what a move left behind
9
13
  */
10
14
  import { Hono } from "hono";
11
- import type { CollectionConfig, DataDriver } from "@rebasepro/types";
15
+ import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
12
16
  import type { HonoEnv } from "../api/types.js";
17
+ import type { JobQueueClient } from "../jobs/types.js";
13
18
  import type { StorageController } from "./types.js";
14
19
  import type { StorageRegistry } from "./storage-registry.js";
20
+ import { type OrphanScanService } from "./orphan-scans.js";
15
21
  export interface StorageMaintenanceRoutesConfig {
16
22
  registry?: StorageRegistry;
17
23
  controller?: StorageController;
18
24
  /** The collections whose rows are read for references, read per request. */
19
25
  collections: () => readonly CollectionConfig[];
20
- /** The unscoped driver serving a collection: every row, whoever wrote it. */
26
+ /**
27
+ * The unscoped driver serving a collection: every row, whoever wrote it.
28
+ * Its `scanStoredRows` is read when it has one — rows as stored, past
29
+ * every read hook.
30
+ */
21
31
  driverFor: (collection: CollectionConfig) => DataDriver | undefined;
32
+ /** The global callbacks, read per request: a global read hook makes every collection hooked. */
33
+ globalCallbacks?: () => CollectionCallbacks | undefined;
34
+ /** Row history, when kept: a file only an entry names is restored by a revert. */
35
+ history?: {
36
+ scanStoredValues?(pageSize?: number): AsyncIterable<unknown[]>;
37
+ };
38
+ /** The durable job queue a scan runs on, read per scan. Absent: scans run in this process. */
39
+ jobs?: () => JobQueueClient | undefined;
40
+ /**
41
+ * The scan service, when the backend shares one with the job queue's
42
+ * `storage.orphans` handler. Built from the fields above when left out.
43
+ */
44
+ orphanScans?: OrphanScanService;
22
45
  }
23
46
  export declare function createStorageMaintenanceRoutes(config: StorageMaintenanceRoutesConfig): Hono<HonoEnv>;
@@ -11,7 +11,7 @@
11
11
  * whichever property holds it. A file is reported only when nothing points at
12
12
  * it at all.
13
13
  */
14
- import type { CollectionConfig, DataDriver } from "@rebasepro/types";
14
+ import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
15
15
  import type { StorageController } from "./types.js";
16
16
  /** One stored object, as a listing describes it. */
17
17
  export interface StoredObjectEntry {
@@ -51,6 +51,12 @@ export interface UnscannedCollection {
51
51
  collection: string;
52
52
  reason: string;
53
53
  }
54
+ /**
55
+ * Whether reading a collection through `fetchCollection` could hide a row or
56
+ * change a value: a `beforeQuery` or an `afterRead`, the collection's own, a
57
+ * global one, or a property's.
58
+ */
59
+ export declare function hasReadHooks(collection: CollectionConfig, globalCallbacks?: CollectionCallbacks): boolean;
54
60
  /**
55
61
  * Remove from `keys` every key some row names.
56
62
  *
@@ -59,13 +65,28 @@ export interface UnscannedCollection {
59
65
  * serves the collection. What is left in `keys` afterwards is named by no row.
60
66
  * A collection that could not be read is returned, and its rows count for
61
67
  * nothing: a caller must not treat what is left as unused when any is.
68
+ *
69
+ * Rows are read as stored, through the driver's `scanStoredRows`. A driver
70
+ * without it is read through `fetchCollection` only for a collection with no
71
+ * read hooks: a `beforeQuery` scoping rows to `context.user` — there is none
72
+ * here — or an `afterRead` rewriting a file field would hide a reference, and
73
+ * the file it names would be deleted as unused. Such a collection is
74
+ * returned as unscanned instead.
62
75
  */
63
76
  export declare function removeReferencedKeys(keys: Set<string>, collections: readonly CollectionConfig[], driverFor: (collection: CollectionConfig) => DataDriver | undefined, options?: {
64
77
  pageSize?: number;
78
+ globalCallbacks?: CollectionCallbacks;
79
+ /** Counted into as it goes, and `onProgress` called after each page. */
80
+ progress?: ScanProgress;
81
+ onProgress?: () => void;
82
+ /** Checked between pages: an aborted scan stops, and throws. */
83
+ signal?: AbortSignal;
65
84
  }): Promise<{
66
85
  scanned: string[];
67
86
  unscanned: UnscannedCollection[];
68
87
  }>;
88
+ /** How row history is named among the scanned and unscanned collections. */
89
+ export declare const HISTORY_SCAN_NAME = "(row history)";
69
90
  /** What `findOrphans` found. */
70
91
  export interface OrphanReport {
71
92
  /** Objects in the source, internal ones aside. */
@@ -80,6 +101,42 @@ export interface OrphanReport {
80
101
  scanned: string[];
81
102
  unscanned: UnscannedCollection[];
82
103
  }
104
+ /** Row history, as a scan reads it. */
105
+ export interface StoredHistory {
106
+ scanStoredValues?(pageSize?: number): AsyncIterable<unknown[]>;
107
+ }
108
+ /** Where references to stored files are read from: every row, and history. */
109
+ export interface ReferenceSources {
110
+ collections: readonly CollectionConfig[];
111
+ driverFor: (collection: CollectionConfig) => DataDriver | undefined;
112
+ /** The callbacks every collection runs, which may scope or rewrite its reads. */
113
+ globalCallbacks?: CollectionCallbacks;
114
+ /**
115
+ * Row history, when the backend keeps it: a revert restores the values an
116
+ * entry holds, so a file only history names is still in use. One that
117
+ * cannot be read is reported unscanned, as {@link HISTORY_SCAN_NAME}.
118
+ */
119
+ history?: StoredHistory;
120
+ }
121
+ /** How far a scan has got. Counted into in place, so a caller can report it while it runs. */
122
+ export interface ScanProgress {
123
+ phase: "listing" | "rows" | "history" | "deleting" | "done";
124
+ /** Objects listed in the source so far. */
125
+ filesScanned: number;
126
+ /** Rows read so far, over every collection. */
127
+ rowsScanned: number;
128
+ /** History entries read so far. */
129
+ historyEntriesScanned: number;
130
+ /** Collections finished, read or not. */
131
+ collectionsScanned: number;
132
+ collectionsTotal: number;
133
+ /** The collection being read now. */
134
+ currentCollection?: string;
135
+ /** The collections that could not be read, as they are found. */
136
+ unscanned: UnscannedCollection[];
137
+ }
138
+ /** A progress record at the start of a scan. */
139
+ export declare function emptyScanProgress(): ScanProgress;
83
140
  /**
84
141
  * The objects of one source that no row names.
85
142
  *
@@ -87,13 +144,52 @@ export interface OrphanReport {
87
144
  * an orphan: the admin panel uploads a file before the row that names it
88
145
  * is saved. One whose age the listing does not say is not either.
89
146
  */
90
- export declare function findOrphans(options: {
147
+ export declare function findOrphans(options: ReferenceSources & {
91
148
  controller: StorageController;
92
- collections: readonly CollectionConfig[];
93
- driverFor: (collection: CollectionConfig) => DataDriver | undefined;
94
149
  olderThanMs: number;
95
150
  now?: number;
151
+ /** Counted into as the scan goes; `onProgress` is called after each page. */
152
+ progress?: ScanProgress;
153
+ onProgress?: () => void;
154
+ /** Checked between pages: an aborted scan stops, and throws. */
155
+ signal?: AbortSignal;
96
156
  }): Promise<OrphanReport>;
157
+ /** What deleting a scan's orphans did. */
158
+ export interface OrphanDeletion {
159
+ deleted: string[];
160
+ /** Candidates not deleted, because something changed since the scan. */
161
+ kept: {
162
+ key: string;
163
+ reason: string;
164
+ }[];
165
+ failed: {
166
+ key: string;
167
+ error: string;
168
+ }[];
169
+ /** Collections the re-check could not read; when any is, nothing was deleted. */
170
+ unscanned: UnscannedCollection[];
171
+ }
172
+ /**
173
+ * Delete the orphans an earlier scan found — each only if it is still one.
174
+ *
175
+ * Every row and history entry is read again, against the candidates alone
176
+ * (the source is not listed again): a row written since the scan may name one
177
+ * now. When a collection cannot be read, nothing is deleted. Then, right
178
+ * before each delete, the object is asked for again: one that is gone, was
179
+ * written again since the scan (another size or time), or is now inside the
180
+ * grace period is kept.
181
+ */
182
+ export declare function deleteScannedOrphans(options: ReferenceSources & {
183
+ controller: StorageController;
184
+ candidates: readonly StoredObjectEntry[];
185
+ olderThanMs: number;
186
+ now?: () => number;
187
+ progress?: ScanProgress;
188
+ onProgress?: () => void;
189
+ /** Called after each file deleted. */
190
+ onDeleted?: (key: string) => void;
191
+ signal?: AbortSignal;
192
+ }): Promise<OrphanDeletion>;
97
193
  /** What happened to one object in a move. */
98
194
  export interface MovedObject {
99
195
  key: string;
@@ -124,3 +220,39 @@ export declare function moveObjects(options: {
124
220
  moved: MovedObject[];
125
221
  cursor?: string;
126
222
  }>;
223
+ /** A file a move did not move, and why it counts as not moved. */
224
+ export interface NotMovedObject {
225
+ key: string;
226
+ reason: string;
227
+ }
228
+ /**
229
+ * List the source again after a move and name every file it did not move,
230
+ * `limit` source objects per call; hand the returned `cursor` back for the
231
+ * next ones.
232
+ *
233
+ * Defence in depth: a move reports what its own walk saw, and a walk that
234
+ * skipped files (as the local listing's page token once did, over a folder
235
+ * the move was emptying) reported success over the half it moved. A fresh
236
+ * listing does not share the walk's state. With `deleteSource`, every file
237
+ * still in the source is not moved; without it, a file the target holds no
238
+ * copy of the same size of.
239
+ */
240
+ export declare function verifyMove(options: {
241
+ from: StorageController;
242
+ to: StorageController;
243
+ prefix?: string;
244
+ cursor?: string;
245
+ limit?: number;
246
+ deleteSource?: boolean;
247
+ }): Promise<{
248
+ checked: number;
249
+ notMoved: NotMovedObject[];
250
+ cursor?: string;
251
+ }>;
252
+ /**
253
+ * Whether two sources store in the same place — one bucket or directory under
254
+ * two names. Asked by their `storageLocation` when both can say; otherwise by
255
+ * a probe object written to `a` under the internal folder, looked for in `b`,
256
+ * and deleted.
257
+ */
258
+ export declare function storesInSamePlace(a: StorageController, b: StorageController): Promise<boolean>;
@@ -0,0 +1,120 @@
1
+ import type { CollectionCallbacks, CollectionConfig, DataDriver } from "@rebasepro/types";
2
+ import type { JobQueueClient } from "../jobs/types.js";
3
+ import type { StorageController } from "./types.js";
4
+ import type { StorageRegistry } from "./storage-registry.js";
5
+ import { type OrphanReport, type ScanProgress, type StoredHistory } from "./maintenance.js";
6
+ /** The job task a scan, or a scan's delete, runs as. Frozen: workers of another release claim it by name. */
7
+ export declare const STORAGE_ORPHANS_TASK = "storage.orphans";
8
+ /** Where scan records are kept inside the scanned source. */
9
+ export declare const ORPHAN_SCAN_FOLDER = "_rebase/maintenance/orphan-scans";
10
+ /**
11
+ * How long after it completed a scan's result may still be deleted from.
12
+ * Every candidate is checked again before it goes, so this is not what keeps
13
+ * a file in use; it keeps a delete acting on what the person just read.
14
+ */
15
+ export declare const ORPHAN_SCAN_TTL_MS: number;
16
+ /**
17
+ * A scan, or a delete, that has written no progress for this long is reported
18
+ * failed: the process running it stopped — a deploy, a crash — and nothing
19
+ * else will finish it. Progress is written at least once a page.
20
+ */
21
+ export declare const ORPHAN_SCAN_STALL_MS: number;
22
+ /** The default grace period: an upload whose row is not saved yet is a day old at most. */
23
+ export declare const DEFAULT_OLDER_THAN_HOURS = 24;
24
+ /** Who runs a scan: the job queue's workers, or the server process that was asked. */
25
+ export type OrphanScanRunner = "worker" | "in-process";
26
+ export type OrphanScanStatus = "queued" | "running" | "completed" | "failed";
27
+ /** A delete from a completed scan, as its record holds it. */
28
+ export interface OrphanScanDeletion {
29
+ status: OrphanScanStatus;
30
+ runner: OrphanScanRunner;
31
+ jobId?: string;
32
+ requestedAt: string;
33
+ startedAt?: string;
34
+ updatedAt: string;
35
+ completedAt?: string;
36
+ progress: ScanProgress & {
37
+ candidates: number;
38
+ deleted: number;
39
+ };
40
+ result?: {
41
+ deleted: string[];
42
+ kept: {
43
+ key: string;
44
+ reason: string;
45
+ }[];
46
+ failed: {
47
+ key: string;
48
+ error: string;
49
+ }[];
50
+ };
51
+ error?: string;
52
+ /** Set on the way out when a running delete has stopped reporting. */
53
+ note?: string;
54
+ }
55
+ /** A scan's record: what `GET /orphans/scans/:id` answers. */
56
+ export interface OrphanScanState {
57
+ id: string;
58
+ storageId: string;
59
+ olderThanHours: number;
60
+ runner: OrphanScanRunner;
61
+ jobId?: string;
62
+ status: OrphanScanStatus;
63
+ createdAt: string;
64
+ startedAt?: string;
65
+ updatedAt: string;
66
+ completedAt?: string;
67
+ progress: ScanProgress;
68
+ /** The report, once completed: what the synchronous `GET /orphans` answered. */
69
+ result?: OrphanReport;
70
+ error?: string;
71
+ /** Said on the way out, never stored: why a queued scan may not have started, or that one stalled. */
72
+ note?: string;
73
+ deletion?: OrphanScanDeletion;
74
+ }
75
+ /** A `storage.orphans` job's payload. */
76
+ export interface OrphanScanTaskPayload {
77
+ scanId: string;
78
+ storageId: string;
79
+ mode: "scan" | "delete";
80
+ }
81
+ export interface OrphanScanServiceConfig {
82
+ registry?: StorageRegistry;
83
+ controller?: StorageController;
84
+ /** The collections whose rows are read for references, read when a scan runs. */
85
+ collections: () => readonly CollectionConfig[];
86
+ /** The unscoped driver serving a collection. See `StorageMaintenanceRoutesConfig.driverFor`. */
87
+ driverFor: (collection: CollectionConfig) => DataDriver | undefined;
88
+ globalCallbacks?: () => CollectionCallbacks | undefined;
89
+ history?: StoredHistory;
90
+ /**
91
+ * The durable job queue, read when a scan starts: present, the scan is a
92
+ * `storage.orphans` job its workers run; absent, it runs in this process.
93
+ */
94
+ jobs?: () => JobQueueClient | undefined;
95
+ /** For tests. */
96
+ now?: () => number;
97
+ }
98
+ export interface OrphanScanService {
99
+ /** Record a scan and set it going. Resolves with its record, `queued`. */
100
+ start(options: {
101
+ storageId?: string;
102
+ olderThanHours?: number;
103
+ }): Promise<OrphanScanState>;
104
+ /** A scan's record, or `undefined` when this source holds none by that id. */
105
+ status(storageId: string | undefined, id: string): Promise<OrphanScanState | undefined>;
106
+ /** Start deleting a completed scan's orphans. Throws the refusal when it may not. */
107
+ requestDeletion(storageId: string | undefined, id: string): Promise<OrphanScanState>;
108
+ /** Run a `storage.orphans` job: the scan, or its delete. */
109
+ runTask(payload: OrphanScanTaskPayload, signal?: AbortSignal): Promise<void>;
110
+ }
111
+ /**
112
+ * The storage source a request names, or the 404 that lists the ones served.
113
+ */
114
+ export declare function resolveStorageSource(sources: {
115
+ registry?: StorageRegistry;
116
+ controller?: StorageController;
117
+ }, rawId: unknown, label: string): StorageController;
118
+ /** `olderThanHours` as a request sends it, checked. */
119
+ export declare function parseOlderThanHours(raw: string | number | undefined): number;
120
+ export declare function createOrphanScanService(config: OrphanScanServiceConfig): OrphanScanService;
@@ -210,6 +210,17 @@ export interface StorageController {
210
210
  * when the caller names none, and each provider maps it to its own.
211
211
  */
212
212
  knownBuckets?(): string[];
213
+ /**
214
+ * Where this controller's default bucket physically is, as a string two
215
+ * controllers return alike exactly when they store in the same place: a
216
+ * resolved directory, an endpoint and bucket.
217
+ *
218
+ * Two sources declared over one bucket or directory are one store under
219
+ * two names, and `rebase storage move --delete-source` between them found
220
+ * every object "already there" — it is the same object — and deleted it
221
+ * from both. A controller without this is found out by a probe object.
222
+ */
223
+ storageLocation?(): string;
213
224
  /**
214
225
  * The largest file this controller accepts, in bytes.
215
226
  *
@@ -0,0 +1,96 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ var snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;
6
+ var toSnakeCase = (str) => {
7
+ if (!str || typeof str !== "string") return "";
8
+ const regExpMatchArray = str.match(snakeCaseRegex);
9
+ if (!regExpMatchArray) return "";
10
+ return regExpMatchArray.map((x) => x.toLowerCase()).join("_");
11
+ };
12
+ /**
13
+ * Are these two identifiers one edit apart, ignoring case?
14
+ *
15
+ * Tight on purpose. A suggester that fires on two edits offers `columnWidth`
16
+ * for `colWith` and `readOnly` for `required`, and a "did you mean" that is
17
+ * usually wrong teaches people to stop reading them. One edit covers what
18
+ * actually happens — a dropped letter (`multilne`), a doubled one, a transposed
19
+ * pair (`validaton`), a wrong case — and nothing else.
20
+ *
21
+ * Lives here rather than beside either caller because both the CLI's manifest
22
+ * reader and the server's collection validator answer the same question about
23
+ * an unknown key, and two copies of a similarity rule is two rules the moment
24
+ * one of them is tuned.
25
+ */
26
+ function isNearMiss(a, b) {
27
+ const x = a.toLowerCase();
28
+ const y = b.toLowerCase();
29
+ if (x === y) return true;
30
+ if (Math.abs(x.length - y.length) > 1) return false;
31
+ const [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];
32
+ let i = 0;
33
+ let j = 0;
34
+ let edits = 0;
35
+ while (i < shorter.length && j < longer.length) {
36
+ if (shorter[i] === longer[j]) {
37
+ i++;
38
+ j++;
39
+ continue;
40
+ }
41
+ if (++edits > 1) return false;
42
+ if (shorter.length === longer.length) i++;
43
+ j++;
44
+ }
45
+ return edits + (longer.length - j) + (shorter.length - i) <= 1;
46
+ }
47
+ /**
48
+ * The key in `known` that `key` was probably meant to be, if any.
49
+ *
50
+ * `undefined` when nothing is close — which is the common case for a key that
51
+ * is simply from a newer version, and the reason the caller must be able to say
52
+ * "unknown" without saying "wrong".
53
+ */
54
+ function suggestNearMiss(known, key) {
55
+ return known.find((candidate) => isNearMiss(candidate, key));
56
+ }
57
+ /**
58
+ * The terms a user meant when they typed into a search box.
59
+ *
60
+ * A search box is not a substring box. Typing `sebastian melendez` on a
61
+ * collection whose name is split across `first_name` and `last_name` has to
62
+ * find the row, and matching the whole string against each field separately
63
+ * cannot: no single column holds both words. Splitting on whitespace and
64
+ * requiring *every* term to match *some* field is what does, and it is what
65
+ * `websearch_to_tsquery` already does for collections that declared a `search`
66
+ * block — so this is also what keeps the two search paths answering the same
67
+ * question.
68
+ *
69
+ * It fixes the trailing space too: `sebastian ` matched nothing at all, because
70
+ * the space was part of the pattern.
71
+ *
72
+ * A double-quoted run is one term, again matching the full-text path, so a
73
+ * caller who really does want the phrase can ask for it. An unbalanced quote is
74
+ * stripped rather than honoured — the alternative is that everything after a
75
+ * stray `"` silently becomes one long term that matches nothing.
76
+ *
77
+ * Returns an empty array for a string with no terms in it at all (whitespace,
78
+ * or nothing but quotes); the caller decides what an empty search means, since
79
+ * only it knows whether "no constraint" or "no rows" is the safe reading.
80
+ */
81
+ function splitSearchTerms(searchString, maxTerms = 8) {
82
+ const terms = [];
83
+ const pattern = /"([^"]*)"|(\S+)/g;
84
+ let match;
85
+ while ((match = pattern.exec(searchString)) !== null) {
86
+ const term = (match[1] ?? match[2].replace(/"/g, "")).trim();
87
+ if (!term) continue;
88
+ terms.push(term);
89
+ if (terms.length >= maxTerms) break;
90
+ }
91
+ return terms;
92
+ }
93
+ //#endregion
94
+ export { suggestNearMiss as n, toSnakeCase as r, splitSearchTerms as t };
95
+
96
+ //# sourceMappingURL=strings.es-B7QLnHh2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.es-B7QLnHh2.js","names":[],"sources":["../../utils/dist/strings.es.js"],"sourcesContent":["//#region src/strings.ts\nvar tokenizeRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;\nvar toKebabCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(tokenizeRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"-\");\n};\nvar snakeCaseRegex = tokenizeRegex;\nvar toSnakeCase = (str) => {\n\tif (!str || typeof str !== \"string\") return \"\";\n\tconst regExpMatchArray = str.match(snakeCaseRegex);\n\tif (!regExpMatchArray) return \"\";\n\treturn regExpMatchArray.map((x) => x.toLowerCase()).join(\"_\");\n};\nfunction camelCase(str) {\n\tif (!str) return \"\";\n\tif (str.length === 1) return str.toLowerCase();\n\tconst parts = str.split(/[-_ ]+/).filter(Boolean);\n\tif (parts.length === 0) return \"\";\n\treturn parts[0].toLowerCase() + parts.slice(1).map((part) => part.charAt(0).toUpperCase() + part.substring(1).toLowerCase()).join(\"\");\n}\n/**\n* A random base-36 string of exactly `strLength` characters.\n*\n* Not `Math.random().toString(36).slice(2, 2 + strLength)`: that has no\n* guaranteed length. Base-36 of a double drops trailing zeros, so the source\n* string is short about once in 36 calls and the slice quietly returns fewer\n* characters than asked for — `randomString(10)` returning 9. These values\n* prefix uploaded filenames to keep them apart, so a short one is a likelier\n* collision, and it fails at the rate that makes a test look flaky.\n*/\nfunction randomString(strLength = 5) {\n\tconst alphabet = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\tlet result = \"\";\n\tfor (let i = 0; i < strLength; i++) result += alphabet.charAt(Math.floor(Math.random() * 36));\n\treturn result;\n}\nfunction randomColor() {\n\treturn Math.floor(Math.random() * 16777215).toString(16);\n}\nfunction slugify(text, separator = \"_\", lowercase = true) {\n\tif (!text) return \"\";\n\tconst from = \"ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;-\";\n\tconst to = `aaaaaeeeeeiiiiooooouuuunc${separator}${separator}${separator}${separator}${separator}${separator}${separator}`;\n\tfor (let i = 0, l = 32; i < l; i++) text = text.replace(new RegExp(from.charAt(i), \"g\"), to.charAt(i));\n\ttext = text.toString().trim().replace(/^\\s+|\\s+$/g, \"\").replace(/\\s+/g, separator).replace(/&/g, separator).replace(/[^\\w\\\\-]+/g, \"\").replace(new RegExp(\"\\\\\" + separator + \"\\\\\" + separator + \"+\", \"g\"), separator);\n\treturn lowercase ? text.toLowerCase() : text;\n}\nfunction unslugify(slug) {\n\tif (!slug) return \"\";\n\tif (slug.includes(\"-\") || slug.includes(\"_\") || !slug.includes(\" \")) return slug.replace(/[-_]/g, \" \").replace(/\\w\\S*/g, function(txt) {\n\t\treturn txt.charAt(0).toUpperCase() + txt.substring(1);\n\t}).trim();\n\telse return slug.trim();\n}\n/**\n* Are these two identifiers one edit apart, ignoring case?\n*\n* Tight on purpose. A suggester that fires on two edits offers `columnWidth`\n* for `colWith` and `readOnly` for `required`, and a \"did you mean\" that is\n* usually wrong teaches people to stop reading them. One edit covers what\n* actually happens — a dropped letter (`multilne`), a doubled one, a transposed\n* pair (`validaton`), a wrong case — and nothing else.\n*\n* Lives here rather than beside either caller because both the CLI's manifest\n* reader and the server's collection validator answer the same question about\n* an unknown key, and two copies of a similarity rule is two rules the moment\n* one of them is tuned.\n*/\nfunction isNearMiss(a, b) {\n\tconst x = a.toLowerCase();\n\tconst y = b.toLowerCase();\n\tif (x === y) return true;\n\tif (Math.abs(x.length - y.length) > 1) return false;\n\tconst [shorter, longer] = x.length <= y.length ? [x, y] : [y, x];\n\tlet i = 0;\n\tlet j = 0;\n\tlet edits = 0;\n\twhile (i < shorter.length && j < longer.length) {\n\t\tif (shorter[i] === longer[j]) {\n\t\t\ti++;\n\t\t\tj++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (++edits > 1) return false;\n\t\tif (shorter.length === longer.length) i++;\n\t\tj++;\n\t}\n\treturn edits + (longer.length - j) + (shorter.length - i) <= 1;\n}\n/**\n* The key in `known` that `key` was probably meant to be, if any.\n*\n* `undefined` when nothing is close — which is the common case for a key that\n* is simply from a newer version, and the reason the caller must be able to say\n* \"unknown\" without saying \"wrong\".\n*/\nfunction suggestNearMiss(known, key) {\n\treturn known.find((candidate) => isNearMiss(candidate, key));\n}\nfunction prettifyIdentifier(input) {\n\tif (!input) return \"\";\n\tlet text = input;\n\ttext = text.replace(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/g, \"$1$3 $2$4\");\n\ttext = text.replace(/[_-]+/g, \" \");\n\treturn text.trim().replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n/**\n* How many terms one search string is allowed to contribute.\n*\n* Every term multiplies the work: the substring fallback compiles to one\n* `ILIKE '%term%'` per term *per searchable column*, evaluated per row on a\n* sequential scan. Eight is far past what a person types into a search box and\n* still bounds a pasted paragraph to a fixed number of predicates. Terms past\n* the cap are dropped rather than rejected — dropping only widens the result\n* set, and a search box that errors on a long paste is worse than one that\n* matches on its first eight words.\n*/\nvar MAX_SEARCH_TERMS = 8;\n/**\n* The terms a user meant when they typed into a search box.\n*\n* A search box is not a substring box. Typing `sebastian melendez` on a\n* collection whose name is split across `first_name` and `last_name` has to\n* find the row, and matching the whole string against each field separately\n* cannot: no single column holds both words. Splitting on whitespace and\n* requiring *every* term to match *some* field is what does, and it is what\n* `websearch_to_tsquery` already does for collections that declared a `search`\n* block — so this is also what keeps the two search paths answering the same\n* question.\n*\n* It fixes the trailing space too: `sebastian ` matched nothing at all, because\n* the space was part of the pattern.\n*\n* A double-quoted run is one term, again matching the full-text path, so a\n* caller who really does want the phrase can ask for it. An unbalanced quote is\n* stripped rather than honoured — the alternative is that everything after a\n* stray `\"` silently becomes one long term that matches nothing.\n*\n* Returns an empty array for a string with no terms in it at all (whitespace,\n* or nothing but quotes); the caller decides what an empty search means, since\n* only it knows whether \"no constraint\" or \"no rows\" is the safe reading.\n*/\nfunction splitSearchTerms(searchString, maxTerms = MAX_SEARCH_TERMS) {\n\tconst terms = [];\n\tconst pattern = /\"([^\"]*)\"|(\\S+)/g;\n\tlet match;\n\twhile ((match = pattern.exec(searchString)) !== null) {\n\t\tconst term = (match[1] ?? match[2].replace(/\"/g, \"\")).trim();\n\t\tif (!term) continue;\n\t\tterms.push(term);\n\t\tif (terms.length >= maxTerms) break;\n\t}\n\treturn terms;\n}\n//#endregion\nexport { MAX_SEARCH_TERMS, camelCase, isNearMiss, prettifyIdentifier, randomColor, randomString, slugify, splitSearchTerms, suggestNearMiss, toKebabCase, toSnakeCase, unslugify };\n\n//# sourceMappingURL=strings.es.js.map"],"mappings":";;;;AAQA,IAAI,iBAAiB;AACrB,IAAI,eAAe,QAAQ;CAC1B,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU,OAAO;CAC5C,MAAM,mBAAmB,IAAI,MAAM,cAAc;CACjD,IAAI,CAAC,kBAAkB,OAAO;CAC9B,OAAO,iBAAiB,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,GAAG;AAC7D;;;;;;;;;;;;;;;AAwDA,SAAS,WAAW,GAAG,GAAG;CACzB,MAAM,IAAI,EAAE,YAAY;CACxB,MAAM,IAAI,EAAE,YAAY;CACxB,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO;CAC9C,MAAM,CAAC,SAAS,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAC/D,IAAI,IAAI;CACR,IAAI,IAAI;CACR,IAAI,QAAQ;CACZ,OAAO,IAAI,QAAQ,UAAU,IAAI,OAAO,QAAQ;EAC/C,IAAI,QAAQ,OAAO,OAAO,IAAI;GAC7B;GACA;GACA;EACD;EACA,IAAI,EAAE,QAAQ,GAAG,OAAO;EACxB,IAAI,QAAQ,WAAW,OAAO,QAAQ;EACtC;CACD;CACA,OAAO,SAAS,OAAO,SAAS,MAAM,QAAQ,SAAS,MAAM;AAC9D;;;;;;;;AAQA,SAAS,gBAAgB,OAAO,KAAK;CACpC,OAAO,MAAM,MAAM,cAAc,WAAW,WAAW,GAAG,CAAC;AAC5D;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,iBAAiB,cAAc,WAAA,GAA6B;CACpE,MAAM,QAAQ,CAAC;CACf,MAAM,UAAU;CAChB,IAAI;CACJ,QAAQ,QAAQ,QAAQ,KAAK,YAAY,OAAO,MAAM;EACrD,MAAM,QAAQ,MAAM,MAAM,MAAM,EAAE,CAAC,QAAQ,MAAM,EAAE,EAAA,CAAG,KAAK;EAC3D,IAAI,CAAC,MAAM;EACX,MAAM,KAAK,IAAI;EACf,IAAI,MAAM,UAAU,UAAU;CAC/B;CACA,OAAO;AACR"}