headroom-cms 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/admin/assets/{AdminsPage-Bt_ekZen.js → AdminsPage-BIWASote.js} +1 -1
  2. package/admin/assets/AllContentPage-1gXe2OC7.js +1 -0
  3. package/admin/assets/{ApiKeysPage-BfWCxGhC.js → ApiKeysPage-BBW4ATBx.js} +1 -1
  4. package/admin/assets/AuditPage-B5GGFWGG.js +1 -0
  5. package/admin/assets/BlockEditor-CQpF8tYb.css +1 -0
  6. package/admin/assets/BlockEditor-ClskiZoX.js +176 -0
  7. package/admin/assets/BlockTypeEditPage-CY0gCPei.js +1 -0
  8. package/admin/assets/BlockTypesPage-D8Me6OeX.js +1 -0
  9. package/admin/assets/{BulkActionBar-TRiXXLQd.js → BulkActionBar--35xjnOP.js} +1 -1
  10. package/admin/assets/CollectionEditPage-y8t0ZO89.js +1 -0
  11. package/admin/assets/{CollectionsPage-ClplrxNn.js → CollectionsPage-BQmGXpvW.js} +1 -1
  12. package/admin/assets/{ContentCreatePage-DfYcEH1u.js → ContentCreatePage-DlgxamOe.js} +1 -1
  13. package/admin/assets/ContentEditPage-WkSbCnnG.js +1 -0
  14. package/admin/assets/ContentField-D04Uo1Ov.js +1 -0
  15. package/admin/assets/ContentListPage-BDMx7pWb.js +1 -0
  16. package/admin/assets/CustomBlockPreview-Cs9bFDh4.js +479 -0
  17. package/admin/assets/FieldRenderer-wE-mtqZB.js +2 -0
  18. package/admin/assets/FilterBar-kFcOLffg.js +1 -0
  19. package/admin/assets/FloatingComposerController-D4uLQfUX-C0Lhbmda.js +1 -0
  20. package/admin/assets/IconPicker-BrgSAsa_.js +3 -0
  21. package/admin/assets/{LoginPage-DutieANA.js → LoginPage-Bi7TBzK4.js} +1 -1
  22. package/admin/assets/MediaField-B-Cz8TlK.js +1 -0
  23. package/admin/assets/MediaPage-C84p9d1U.js +1 -0
  24. package/admin/assets/Pagination-CuHwUPHi.js +1 -0
  25. package/admin/assets/RelationshipPicker-Dv7GaLcU.js +1 -0
  26. package/admin/assets/{SiteSettingsPage-BtCC3RKc.js → SiteSettingsPage-nBT7NzkA.js} +1 -1
  27. package/admin/assets/{SiteUserEditPage-ClHmp0T-.js → SiteUserEditPage-DroUTii9.js} +1 -1
  28. package/admin/assets/SiteUsersPage-iVXPCBPe.js +1 -0
  29. package/admin/assets/{SitesPage-Bw_WBN6v.js → SitesPage-BefZeWuJ.js} +1 -1
  30. package/admin/assets/{SubmissionDetailPage-DS08LGxd.js → SubmissionDetailPage-ktmzzOE1.js} +1 -1
  31. package/admin/assets/SubmissionEditPage-C-ykTI2t.js +1 -0
  32. package/admin/assets/SubmissionListPage-DA-8deUy.js +1 -0
  33. package/admin/assets/{TagInput-BILCaC9b.js → TagInput-d-Hw1fkL.js} +1 -1
  34. package/admin/assets/{TagsPage-DdeZokow.js → TagsPage-BZzDvcKa.js} +1 -1
  35. package/admin/assets/{UsersPage-B0vLxjrg.js → UsersPage-CnQAOOGF.js} +1 -1
  36. package/admin/assets/{WebhookEditPage-SlJE4d3z.js → WebhookEditPage-KeS8hmdW.js} +1 -1
  37. package/admin/assets/{WebhooksPage-C6lGZLpr.js → WebhooksPage-CASjmlPN.js} +1 -1
  38. package/admin/assets/{card-hXVtlM0q.js → card-CZTHR2Qa.js} +1 -1
  39. package/admin/assets/checkbox-DEgzM8H9.js +1 -0
  40. package/admin/assets/{collapsible-B414SspL.js → collapsible-D3d29uJp.js} +1 -1
  41. package/admin/assets/{command-fvBFHye4.js → command-CdzYw11U.js} +1 -1
  42. package/admin/assets/contentStatus-CkPi9Dh6.js +1 -0
  43. package/admin/assets/{core.esm-B_kcYf6n.js → core.esm-DdQHdRkd.js} +2 -2
  44. package/admin/assets/index-BA3y7HJs.js +18 -0
  45. package/admin/assets/index-c7UygSvP.css +1 -0
  46. package/admin/assets/popover-BFw_h3j6.js +1 -0
  47. package/admin/assets/radix-C5ZmWuuL.js +51 -0
  48. package/admin/assets/select-dX9e6VDt.js +1 -0
  49. package/admin/assets/{serializeToText-DR_WnxiI.js → serializeToText-Zin3gYPm.js} +1 -1
  50. package/admin/assets/{sortable.esm-QyXA6fio.js → sortable.esm-qVEMoaTg.js} +1 -1
  51. package/admin/assets/{table-DLoIbCQ5.js → table-Dk7eeOt2.js} +1 -1
  52. package/admin/assets/{textarea-vSXNxwTe.js → textarea-CpDSUg2s.js} +1 -1
  53. package/admin/assets/useAdminResolver-Bljb4XGQ.js +1 -0
  54. package/admin/assets/useContent-CW0tm0FY.js +1 -0
  55. package/admin/assets/useContentSearch-_bwacEth.js +1 -0
  56. package/admin/assets/{useMedia-e3sqWm_t.js → useMedia-Cu5N4rY8.js} +1 -1
  57. package/admin/assets/usePageTitle-DYvuJQp6.js +1 -0
  58. package/admin/assets/useSiteUsers-CKtC_8Jc.js +1 -0
  59. package/admin/assets/{useTags-f7AVSLuj.js → useTags-ybsMbCst.js} +1 -1
  60. package/admin/assets/{useWebhooks-BH_r8-Mo.js → useWebhooks-BAB-3sLa.js} +1 -1
  61. package/admin/assets/yjs-tXBm_srz.js +5 -0
  62. package/admin/index.html +3 -3
  63. package/admin/sw.js +1 -1
  64. package/dist/admin-site.d.ts +12 -0
  65. package/dist/admin-site.d.ts.map +1 -1
  66. package/dist/admin-site.js +8 -1
  67. package/dist/admin-site.js.map +1 -1
  68. package/dist/api.d.ts +7 -0
  69. package/dist/api.d.ts.map +1 -1
  70. package/dist/api.js +8 -1
  71. package/dist/api.js.map +1 -1
  72. package/dist/collaboration.d.ts +55 -0
  73. package/dist/collaboration.d.ts.map +1 -0
  74. package/dist/collaboration.js +141 -0
  75. package/dist/collaboration.js.map +1 -0
  76. package/dist/index.d.ts +12 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +29 -4
  79. package/dist/index.js.map +1 -1
  80. package/dist/storage.d.ts +2 -0
  81. package/dist/storage.d.ts.map +1 -1
  82. package/dist/storage.js +33 -0
  83. package/dist/storage.js.map +1 -1
  84. package/lambda/api/bootstrap +0 -0
  85. package/lambda/webhook-worker/bootstrap +0 -0
  86. package/package.json +1 -1
  87. package/src/admin-site.ts +20 -1
  88. package/src/api.ts +15 -1
  89. package/src/collaboration.ts +187 -0
  90. package/src/index.ts +44 -4
  91. package/src/sst-env.d.ts +28 -0
  92. package/src/storage.ts +35 -0
  93. package/admin/assets/AllContentPage-CFqEMAl9.js +0 -1
  94. package/admin/assets/AuditPage-BE0XIUl2.js +0 -1
  95. package/admin/assets/BlockEditor-6wqsThJ7.js +0 -179
  96. package/admin/assets/BlockEditor-Cp_wZ2xN.css +0 -1
  97. package/admin/assets/BlockTypeEditPage-CuNJfZw0.js +0 -1
  98. package/admin/assets/BlockTypesPage-BIMBVxBs.js +0 -1
  99. package/admin/assets/CollectionEditPage-BqX_0cC2.js +0 -1
  100. package/admin/assets/ContentEditPage-D3Rvlktk.js +0 -2
  101. package/admin/assets/ContentListPage-zmO8Is4d.js +0 -1
  102. package/admin/assets/CustomBlockPreview-C6HqS4xv.js +0 -479
  103. package/admin/assets/FieldBuilder-36tfpSyM.js +0 -3
  104. package/admin/assets/FilterBar-DhRwTqFv.js +0 -1
  105. package/admin/assets/MediaField-J2TLG_fu.js +0 -1
  106. package/admin/assets/MediaPage-DZZKMGF4.js +0 -1
  107. package/admin/assets/RelationshipPicker-CDFs4TMW.js +0 -1
  108. package/admin/assets/SiteUsersPage-AyJvcVM7.js +0 -1
  109. package/admin/assets/SubmissionEditPage-Brf-DK2X.js +0 -1
  110. package/admin/assets/SubmissionListPage-DNMzQZHS.js +0 -1
  111. package/admin/assets/checkbox-WGrS3sUr.js +0 -1
  112. package/admin/assets/contentStatus-BmaiYVOm.js +0 -1
  113. package/admin/assets/index-Cir9tY_P.js +0 -18
  114. package/admin/assets/index-DACBYsKM.css +0 -1
  115. package/admin/assets/popover-D5_HjjUC.js +0 -1
  116. package/admin/assets/radix-C1kb_NqW.js +0 -51
  117. package/admin/assets/select-_uJYxzeZ.js +0 -1
  118. package/admin/assets/useAdminResolver-D-LlmquD.js +0 -1
  119. package/admin/assets/useContent-e8beBIuq.js +0 -1
  120. package/admin/assets/useContentSearch-DOjveB9t.js +0 -1
  121. package/admin/assets/useDebouncedValue-C-cQUcLG.js +0 -1
  122. package/admin/assets/usePageTitle-BNSba9_L.js +0 -1
  123. package/admin/assets/useSiteUsers-BdnvuM2E.js +0 -1
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Collaboration Infrastructure
3
+ *
4
+ * API Gateway WebSocket API + DynamoDB Collab table + Node.js Lambda handler.
5
+ * Deployed at all times but dormant for solo editing — no Lambda invocations
6
+ * and no Collab table writes happen until collaboration is opted into.
7
+ *
8
+ * Supports dev mode (Node.js source with live reload) and package mode
9
+ * (pre-bundled handler).
10
+ *
11
+ * Split into two helpers:
12
+ * - `createCollabTable` — provisions just the DynamoDB Collab table.
13
+ * Must be called BEFORE `createApi`, because the Go API links the
14
+ * Collab table for ticket reads/writes.
15
+ * - `createCollabHandler` — provisions the WebSocket API + Lambda and
16
+ * wires `API_URL` into the Lambda environment so `snapshotToDraft`
17
+ * can `fetch` back into the Go API on `$disconnect`. Must be called
18
+ * AFTER `createApi`, because it needs `api.api.url`.
19
+ *
20
+ * The two halves are merged into a single `CollaborationResources` value
21
+ * by `index.ts` so downstream consumers (admin-site, etc.) see one shape.
22
+ */
23
+
24
+ import path from "path";
25
+ import type { StorageResources } from "./storage.js";
26
+ import type { AuthResources } from "./auth.js";
27
+ import type { ApiResources } from "./api.js";
28
+
29
+ export interface CollabTableArgs {
30
+ // No external dependencies — the table is a leaf resource.
31
+ }
32
+
33
+ export function createCollabTable(name: string, _args: CollabTableArgs = {}) {
34
+ // DynamoDB table for collaboration sessions + YJS state
35
+ const collabTable = new sst.aws.Dynamo(`${name}Collab`, {
36
+ fields: {
37
+ pk: "string",
38
+ sk: "string",
39
+ },
40
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
41
+ // PascalCase to match the convention used throughout the collab TS
42
+ // package (RoomManager, YjsPersistence) and the Go ticket writer
43
+ // (repository/collab.go). DynamoDB attribute names are case-sensitive;
44
+ // the TTL attribute name must match what writers actually emit.
45
+ ttl: "ExpiresAt",
46
+ });
47
+
48
+ return { collabTable };
49
+ }
50
+
51
+ export type CollabTableResources = ReturnType<typeof createCollabTable>;
52
+
53
+ export interface CollabHandlerArgs {
54
+ storage: StorageResources;
55
+ auth: AuthResources;
56
+ api: ApiResources;
57
+ collabTable: CollabTableResources["collabTable"];
58
+ pkgRoot: string;
59
+ dev?: {
60
+ /** SST handler string, e.g. "packages/collab/src/index.handler" */
61
+ handler: string;
62
+ };
63
+ }
64
+
65
+ export function createCollabHandler(name: string, args: CollabHandlerArgs) {
66
+ const { storage, auth, api, collabTable } = args;
67
+
68
+ // WebSocket API via raw Pulumi (SST v4 doesn't have a WebSocket component)
69
+ const wsApi = new aws.apigatewayv2.Api(`${name}WsApi`, {
70
+ protocolType: "WEBSOCKET",
71
+ routeSelectionExpression: "$request.body.action",
72
+ });
73
+
74
+ const handlerConfig = args.dev
75
+ ? {
76
+ handler: args.dev.handler,
77
+ runtime: "nodejs22.x" as const,
78
+ }
79
+ : {
80
+ bundle: path.join(args.pkgRoot, "packages/collab"),
81
+ handler: "index.handler",
82
+ runtime: "nodejs22.x" as const,
83
+ };
84
+
85
+ const wsHandler = new sst.aws.Function(`${name}CollabHandler`, {
86
+ ...handlerConfig,
87
+ // jsdom is pulled in transitively via @blocknote/server-util (used in
88
+ // src/yjs/converter.ts to convert YJS state to BlockNote blocks during
89
+ // the disconnect snapshot). esbuild does not auto-bundle jsdom's
90
+ // xhr-sync-worker.js because it's spawned dynamically at runtime
91
+ // (`new Worker(__dirname + "/xhr-sync-worker.js")`), so the worker file
92
+ // is missing on disk in the deployed Lambda and cold-starts fail with
93
+ // "Cannot find module './xhr-sync-worker.js'". Adding jsdom to
94
+ // `nodejs.install` excludes it from the esbuild bundle and instead
95
+ // installs it (with its full directory tree, including the worker file)
96
+ // into the function's node_modules/ at deploy time.
97
+ nodejs: {
98
+ install: ["jsdom"],
99
+ },
100
+ timeout: "30 seconds",
101
+ memory: "512 MB",
102
+ environment: {
103
+ COLLAB_TABLE: collabTable.name,
104
+ DRAFT_CONTENT_TABLE: storage.draftContent.name,
105
+ BLOCKS_TABLE: storage.blocks.name,
106
+ SITES_TABLE: storage.sites.name,
107
+ USER_POOL_ID: auth.userPool.id,
108
+ USER_POOL_CLIENT_ID: auth.userPoolClient.id,
109
+ WS_API_ENDPOINT: $interpolate`https://${wsApi.id}.execute-api.${aws.getRegionOutput().name}.amazonaws.com/${$app.stage}`,
110
+ // Required by `snapshotToDraft` (packages/collab/src/draft/snapshot.ts)
111
+ // which fetches `${apiUrl}/v1/admin/sites/{host}/content/{id}/draft`
112
+ // on `$disconnect` to persist the YJS room state back to DraftContent.
113
+ // Without this, the URL becomes `undefined/v1/admin/...` and the fetch
114
+ // throws "Failed to parse URL", crashing the disconnect Lambda.
115
+ API_URL: api.api.url,
116
+ // Shared with the Go API. Sent as `X-Headroom-Internal` on the
117
+ // disconnect snapshot PUT so the Go JWT middleware accepts the
118
+ // internal service-to-service call. See packages/api/internal/middleware/jwt.go.
119
+ INTERNAL_SECRET: storage.internalSecret.value,
120
+ // Phase 7.1 "Large documents": S3 bucket for YJS state that exceeds
121
+ // DynamoDB's 400KB per-item cap. See `YjsPersistence.saveState`.
122
+ COLLAB_STATE_BUCKET: storage.collabStateBucket.name,
123
+ },
124
+ link: [
125
+ collabTable,
126
+ storage.draftContent,
127
+ storage.blocks,
128
+ storage.sites,
129
+ storage.internalSecret,
130
+ // S3 link grants the Lambda IAM permissions for s3:GetObject /
131
+ // s3:PutObject on the bucket via SST's resource binding.
132
+ storage.collabStateBucket,
133
+ ],
134
+ permissions: [
135
+ {
136
+ actions: ["execute-api:ManageConnections"],
137
+ resources: [$interpolate`arn:aws:execute-api:*:*:${wsApi.id}/*`],
138
+ },
139
+ ],
140
+ });
141
+
142
+ // Wire up $connect, $default, $disconnect routes
143
+ const integration = new aws.apigatewayv2.Integration(
144
+ `${name}WsIntegration`,
145
+ {
146
+ apiId: wsApi.id,
147
+ integrationType: "AWS_PROXY",
148
+ integrationUri: wsHandler.nodes.function.invokeArn,
149
+ },
150
+ );
151
+
152
+ for (const routeKey of ["$connect", "$default", "$disconnect"]) {
153
+ new aws.apigatewayv2.Route(`${name}WsRoute${routeKey}`, {
154
+ apiId: wsApi.id,
155
+ routeKey,
156
+ target: $interpolate`integrations/${integration.id}`,
157
+ });
158
+ }
159
+
160
+ new aws.apigatewayv2.Stage(`${name}WsStage`, {
161
+ apiId: wsApi.id,
162
+ name: $app.stage,
163
+ autoDeploy: true,
164
+ });
165
+
166
+ // Grant API Gateway permission to invoke Lambda
167
+ new aws.lambda.Permission(`${name}WsPermission`, {
168
+ action: "lambda:InvokeFunction",
169
+ function: wsHandler.nodes.function.name,
170
+ principal: "apigateway.amazonaws.com",
171
+ sourceArn: $interpolate`${wsApi.executionArn}/*/*`,
172
+ });
173
+
174
+ const wsUrl =
175
+ $interpolate`wss://${wsApi.id}.execute-api.${aws.getRegionOutput().name}.amazonaws.com/${$app.stage}`;
176
+
177
+ return { wsApi, wsHandler, wsUrl };
178
+ }
179
+
180
+ export type CollabHandlerResources = ReturnType<typeof createCollabHandler>;
181
+
182
+ /**
183
+ * Combined collaboration resource shape — the union of the table half
184
+ * (created early) and the handler half (created late). Downstream consumers
185
+ * (admin-site, public outputs) see a single object.
186
+ */
187
+ export type CollaborationResources = CollabTableResources & CollabHandlerResources;
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { createImage } from "./image.js";
14
14
  import { createApi } from "./api.js";
15
15
  import { createCdn } from "./cdn.js";
16
16
  import { createScheduler } from "./scheduler.js";
17
+ import { createCollabTable, createCollabHandler } from "./collaboration.js";
17
18
  import { createAdminSite } from "./admin-site.js";
18
19
 
19
20
  /**
@@ -50,6 +51,7 @@ export type { ImageResources } from "./image.js";
50
51
  export type { ApiResources } from "./api.js";
51
52
  export type { CdnResources } from "./cdn.js";
52
53
  export type { SchedulerResources } from "./scheduler.js";
54
+ export type { CollaborationResources } from "./collaboration.js";
53
55
  export type { AdminSiteResources } from "./admin-site.js";
54
56
 
55
57
  export interface HeadroomCMSArgs {
@@ -120,7 +122,18 @@ export interface HeadroomCMSArgs {
120
122
  adminPath: string;
121
123
  /** Go source path for scheduler Lambda, e.g. "packages/scheduler" */
122
124
  schedulerHandler?: string;
125
+ /** SST handler for collab Lambda, e.g. "packages/collab/src/index.handler" */
126
+ collabHandler?: string;
123
127
  };
128
+
129
+ /**
130
+ * Build-time toggle for the collaboration feature in the admin UI.
131
+ * Defaults to `true`. Set `false` to keep the WebSocket / Collab table
132
+ * deployed but hide the "Collaborate" / "Request collaboration" buttons
133
+ * and refuse to open WebSockets — a kill-switch without teardown. Used
134
+ * to ship the feature to a stage before turning it on for editors.
135
+ */
136
+ collabEnabled?: boolean;
124
137
  }
125
138
 
126
139
  export class HeadroomCMS {
@@ -129,6 +142,7 @@ export class HeadroomCMS {
129
142
  public readonly adminUrl: $util.Output<string>;
130
143
  public readonly userPoolId: $util.Output<string>;
131
144
  public readonly userPoolClientId: $util.Output<string>;
145
+ public readonly collabWsUrl: $util.Output<string>;
132
146
 
133
147
  public readonly outputs: Record<string, $util.Output<string>>;
134
148
 
@@ -167,12 +181,17 @@ export class HeadroomCMS {
167
181
  : undefined,
168
182
  });
169
183
 
170
- // 5. API Lambda (Go handler with access to all resources)
184
+ // 5a. Collaboration table (must exist before the API Lambda, which
185
+ // links it for ticket reads/writes).
186
+ const collabTable = createCollabTable(name);
187
+
188
+ // 6. API Lambda (Go handler with access to all resources)
171
189
  const api = createApi(name, {
172
190
  storage,
173
191
  auth,
174
192
  webhooks,
175
193
  image,
194
+ collab: collabTable,
176
195
  senderEmail: args.senderEmail,
177
196
  pkgRoot,
178
197
  dev: args.dev
@@ -180,7 +199,24 @@ export class HeadroomCMS {
180
199
  : undefined,
181
200
  });
182
201
 
183
- // 6. Scheduler (EventBridge cron + Go Lambda + lock table)
202
+ // 5b. Collaboration WebSocket handler (must be created AFTER the API
203
+ // because its Lambda environment includes API_URL = api.api.url, which
204
+ // `snapshotToDraft` uses to PUT the disconnect snapshot back to the
205
+ // Go draft endpoint).
206
+ const collabHandler = createCollabHandler(name, {
207
+ storage,
208
+ auth,
209
+ api,
210
+ collabTable: collabTable.collabTable,
211
+ pkgRoot,
212
+ dev: args.dev?.collabHandler
213
+ ? { handler: args.dev.collabHandler }
214
+ : undefined,
215
+ });
216
+
217
+ const collab = { ...collabTable, ...collabHandler };
218
+
219
+ // 7. Scheduler (EventBridge cron + Go Lambda + lock table)
184
220
  const schedulerResources = createScheduler(name, {
185
221
  storage,
186
222
  api,
@@ -190,7 +226,7 @@ export class HeadroomCMS {
190
226
  : undefined,
191
227
  });
192
228
 
193
- // 7. CDN (CloudFront distribution + edge functions)
229
+ // 8. CDN (CloudFront distribution + edge functions)
194
230
  const cdn = createCdn(name, {
195
231
  api,
196
232
  image,
@@ -201,16 +237,18 @@ export class HeadroomCMS {
201
237
  domain: args.domain,
202
238
  });
203
239
 
204
- // 8. Admin UI (static site)
240
+ // 9. Admin UI (static site)
205
241
  const admin = createAdminSite(name, {
206
242
  api,
207
243
  cdn,
208
244
  auth,
245
+ collab,
209
246
  pkgRoot,
210
247
  dev: args.dev
211
248
  ? { adminPath: args.dev.adminPath }
212
249
  : undefined,
213
250
  domain: args.adminDomain,
251
+ collabEnabled: args.collabEnabled,
214
252
  });
215
253
 
216
254
  // Expose outputs
@@ -219,6 +257,7 @@ export class HeadroomCMS {
219
257
  this.adminUrl = admin.url;
220
258
  this.userPoolId = auth.userPool.id;
221
259
  this.userPoolClientId = auth.userPoolClient.id;
260
+ this.collabWsUrl = collab.wsUrl;
222
261
 
223
262
  this.outputs = {
224
263
  api: api.api.url,
@@ -226,6 +265,7 @@ export class HeadroomCMS {
226
265
  admin: admin.url,
227
266
  userPoolId: auth.userPool.id,
228
267
  userPoolClientId: auth.userPoolClient.id,
268
+ collabWs: collab.wsUrl,
229
269
  };
230
270
  }
231
271
  }
package/src/sst-env.d.ts CHANGED
@@ -40,6 +40,13 @@ declare namespace sst {
40
40
  url: PulumiOutput<string>;
41
41
  name: PulumiOutput<string>;
42
42
  arn: PulumiOutput<string>;
43
+ nodes: {
44
+ function: {
45
+ name: PulumiOutput<string>;
46
+ invokeArn: PulumiOutput<string>;
47
+ arn: PulumiOutput<string>;
48
+ };
49
+ };
43
50
  }
44
51
 
45
52
  class CognitoUserPool {
@@ -111,6 +118,27 @@ declare namespace aws {
111
118
  }
112
119
  }
113
120
 
121
+ namespace apigatewayv2 {
122
+ class Api {
123
+ constructor(name: string, args?: any, opts?: any);
124
+ id: PulumiOutput<string>;
125
+ executionArn: PulumiOutput<string>;
126
+ }
127
+
128
+ class Integration {
129
+ constructor(name: string, args?: any, opts?: any);
130
+ id: PulumiOutput<string>;
131
+ }
132
+
133
+ class Route {
134
+ constructor(name: string, args?: any, opts?: any);
135
+ }
136
+
137
+ class Stage {
138
+ constructor(name: string, args?: any, opts?: any);
139
+ }
140
+ }
141
+
114
142
  namespace ses {
115
143
  class EmailIdentity {
116
144
  constructor(name: string, args?: any, opts?: any);
package/src/storage.ts CHANGED
@@ -131,11 +131,44 @@ export function createStorage(name: string) {
131
131
  },
132
132
  });
133
133
 
134
+ // Phase 7.1 "Large documents": YJS room state that exceeds DynamoDB's 400KB
135
+ // per-item cap is uploaded here instead of being inlined on the Collab
136
+ // item. Dedicated bucket (vs reusing `contentBucket`) for two reasons:
137
+ // 1. `contentBucket` is `access: "cloudfront"` for media delivery; YJS
138
+ // state must NOT be CDN-fronted (it's per-collaboration-session
139
+ // transient state, never publicly addressable).
140
+ // 2. Lifecycle expiration is much shorter — these objects are recovery
141
+ // fallback for an active collaboration session; the matching Collab
142
+ // table item carries a 24h DynamoDB TTL, so the S3 lifecycle rule
143
+ // cleans up overflow objects on a similar timeline.
144
+ const collabStateBucket = new sst.aws.Bucket(`${name}CollabStateBucket`, {
145
+ transform: {
146
+ bucket: (args: any) => {
147
+ args.lifecycleRules = [
148
+ {
149
+ id: "expire-collab-state",
150
+ enabled: true,
151
+ expiration: { days: 7 },
152
+ // Keep the matching Collab DynamoDB TTL (24h) as the primary
153
+ // cleanup mechanism; this is a long-stop safety net so orphaned
154
+ // S3 objects don't accumulate forever.
155
+ },
156
+ ];
157
+ },
158
+ },
159
+ });
160
+
134
161
  const kvs = new aws.cloudfront.KeyValueStore(`${name}KVS`, {
135
162
  name: $interpolate`${$app.name}-${$app.stage}-kvs`,
136
163
  comment: "Headroom CMS edge data: API keys and site versions",
137
164
  });
138
165
 
166
+ // Shared secret for internal service-to-service auth between the collab
167
+ // Lambda and the Go API (`X-Headroom-Internal` header). Both Lambdas read
168
+ // this from their `INTERNAL_SECRET` environment variable. Set the value via
169
+ // `sst secret set HeadroomInternalSecret <value>` (or the namespaced equivalent).
170
+ const internalSecret = new sst.Secret(`${name}InternalSecret`);
171
+
139
172
  return {
140
173
  sites,
141
174
  content,
@@ -148,7 +181,9 @@ export function createStorage(name: string) {
148
181
  relationships,
149
182
  siteUsers,
150
183
  contentBucket,
184
+ collabStateBucket,
151
185
  kvs,
186
+ internalSecret,
152
187
  };
153
188
  }
154
189
 
@@ -1 +0,0 @@
1
- import{j as s}from"./tanstack-Bs3zYPPV.js";import{u as U,f as D,r as w}from"./react-vendor-C2CvUxFh.js";import{u as M,a as R,b as L,c as z}from"./useContent-e8beBIuq.js";import{m as k}from"./media-url-DIg_vSyf.js";import{w as E,x as H,v as r,S as F,j as h,y as V,B as _}from"./index-Cir9tY_P.js";import{u as q}from"./usePageTitle-BNSba9_L.js";import{C as G,c as J}from"./card-hXVtlM0q.js";import{C as m}from"./checkbox-WGrS3sUr.js";import{T as K,a as O,b as S,c as i,d as Q,e as d}from"./table-DLoIbCQ5.js";import{f as y}from"./format-C88SDH8g.js";import{B as W}from"./BulkActionBar-TRiXXLQd.js";import"./radix-C1kb_NqW.js";function ie(){const{host:c}=U(),p=D(),{collections:A}=E(),f=H();q({title:"Recent"});const[$,P]=w.useState(),[n,a]=w.useState(new Set),{data:o,isLoading:I}=M(c,$),j=R(c),g=L(c),b=z(c),N=new Map(A.map(e=>[e.name,e.label])),x=o?.items.map(e=>e.contentId)??[],v=x.length>0&&x.every(e=>n.has(e));function C(e){a(t=>{const l=new Set(t);return l.has(e)?l.delete(e):l.add(e),l})}function B(){a(v?new Set:new Set(x))}const T=j.isPending||g.isPending||b.isPending;return s.jsxs("div",{children:[!f&&s.jsxs("div",{className:"mb-6",children:[s.jsx("h1",{className:"text-2xl font-semibold",children:"All Content"}),s.jsx("p",{className:"text-muted-foreground text-sm",children:"Recently updated content across all collections."})]}),n.size>0&&s.jsx(W,{selectedCount:n.size,onPublish:async()=>{const e=[...n],l=(await j.mutateAsync(e)).filter(u=>u.status==="rejected").length;l?r.error(`${l} of ${e.length} failed to publish`):r.success(`${e.length} item(s) published`),a(new Set)},onUnpublish:async()=>{const e=[...n],l=(await g.mutateAsync(e)).filter(u=>u.status==="rejected").length;l?r.error(`${l} of ${e.length} failed to unpublish`):r.success(`${e.length} item(s) unpublished`),a(new Set)},onDelete:async()=>{const e=[...n],l=(await b.mutateAsync(e)).filter(u=>u.status==="rejected").length;l?r.error(`${l} of ${e.length} failed to delete`):r.success(`${e.length} item(s) deleted`),a(new Set)},onClear:()=>a(new Set),isPending:T}),I?s.jsx("div",{className:"space-y-2",children:Array.from({length:5}).map((e,t)=>s.jsx(F,{className:"h-12 w-full"},t))}):o?.items.length?s.jsxs(s.Fragment,{children:[f?s.jsx("div",{className:"grid gap-3","data-testid":"all-content-card-view",children:o.items.map(e=>{const t=e.cover;return s.jsx(G,{className:V("cursor-pointer gap-0 overflow-hidden py-0 transition-shadow hover:shadow-md",n.has(e.contentId)&&"ring-2 ring-primary"),onClick:()=>p(`/sites/${c}/content/${e.collection}/${e.contentId}?from=recent`),children:s.jsx(J,{className:"p-3",children:s.jsxs("div",{className:"flex items-start gap-3 min-w-0",children:[t?.url&&s.jsx("img",{src:k(t.url),alt:t.alt??"",className:"h-12 w-12 rounded object-cover flex-shrink-0"}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[s.jsx("span",{className:"font-medium truncate",children:e.title||"Untitled"}),s.jsx(h,{variant:e.publishedAt?"default":"secondary",className:"shrink-0",children:e.publishedAt?"Published":"Draft"})]}),e.slug&&s.jsxs("p",{className:"text-xs text-muted-foreground truncate mt-0.5",children:["/",e.slug]}),s.jsxs("div",{className:"flex items-center gap-2 mt-1",children:[s.jsx(h,{variant:"outline",className:"text-[10px] px-1.5 py-0",children:N.get(e.collection)??e.collection}),s.jsx("span",{className:"text-xs text-muted-foreground",children:e.publishedAt?y(e.publishedAt):"—"})]})]}),s.jsx(m,{checked:n.has(e.contentId),onCheckedChange:()=>C(e.contentId),onClick:l=>l.stopPropagation(),className:"mt-1"})]})})},e.contentId)})}):s.jsxs(K,{children:[s.jsx(O,{children:s.jsxs(S,{children:[s.jsx(i,{className:"w-10",children:s.jsx(m,{checked:v,onCheckedChange:B,"aria-label":"Select all"})}),s.jsx(i,{children:"Title"}),s.jsx(i,{children:"Slug"}),s.jsx(i,{children:"Collection"}),s.jsx(i,{children:"Status"}),s.jsx(i,{children:"Updated"})]})}),s.jsx(Q,{children:o.items.map(e=>s.jsxs(S,{className:"cursor-pointer",onClick:()=>p(`/sites/${c}/content/${e.collection}/${e.contentId}?from=recent`),children:[s.jsx(d,{onClick:t=>t.stopPropagation(),children:s.jsx(m,{checked:n.has(e.contentId),onCheckedChange:()=>C(e.contentId),"aria-label":`Select ${e.title||"Untitled"}`})}),s.jsx(d,{className:"font-medium",children:s.jsxs("div",{className:"flex items-center gap-3",children:[(()=>{const t=e.cover;return t?.url?s.jsx("img",{src:k(t.url),alt:t.alt??"",className:"h-8 w-8 rounded object-cover flex-shrink-0"}):null})(),s.jsx("span",{children:e.title||"Untitled"})]})}),s.jsx(d,{className:"text-muted-foreground text-sm",children:e.slug||"—"}),s.jsx(d,{children:s.jsx(h,{variant:"outline",children:N.get(e.collection)??e.collection})}),s.jsx(d,{children:s.jsx(h,{variant:e.publishedAt?"default":"secondary",children:e.publishedAt?"Published":"Draft"})}),s.jsx(d,{className:"text-muted-foreground",children:e.publishedAt?y(e.publishedAt):"—"})]},e.contentId))})]}),o.hasMore&&s.jsx("div",{className:"mt-4 flex justify-center",children:s.jsx(_,{variant:"outline",onClick:()=>P(o.cursor??void 0),children:"Load More"})})]}):s.jsx("p",{className:"text-muted-foreground",children:"No content yet."})]})}export{ie as AllContentPage};
@@ -1 +0,0 @@
1
- import{u as F,j as e}from"./tanstack-Bs3zYPPV.js";import{L,r as i,u as E}from"./react-vendor-C2CvUxFh.js";import{e as O,F as K,G as V,J as W,K as q,B as R,au as H,S as y,x as J,aK as z,aJ as v,aL as G,aM as Q,aN as X,aO as Y,aP as Z,aQ as ee,aR as te,aS as se,al as S,aT as ae,T as x,$ as m,aE as le,P as A,aU as U,ai as re,aV as de,aW as ne,aX as oe,a4 as ce,aY as ie,aZ as ue}from"./index-Cir9tY_P.js";import{u as me}from"./useAdminResolver-D-LlmquD.js";import{s as _,T as he}from"./serializeToText-DR_WnxiI.js";import{h as T}from"./useContent-e8beBIuq.js";import{f as P,a as xe}from"./format-C88SDH8g.js";import{S as be,a as pe,b as fe,c as ge,d as je}from"./select-_uJYxzeZ.js";import{T as ke,a as Ie,b as D,c as p,d as ye,e as f}from"./table-DLoIbCQ5.js";import{u as Ce}from"./usePageTitle-BNSba9_L.js";import"./radix-C1kb_NqW.js";function we(t,a){const l=O(),d=new URLSearchParams;a?.action&&d.set("action",a.action),a?.before&&d.set("before",String(a.before));const o=d.toString();return F({queryKey:["sites",t,"audit",a?.action??"",a?.before??""],queryFn:()=>l.apiFetch(`/v1/admin/sites/${t}/audit${o?`?${o}`:""}`),enabled:!!t})}const Ne={"content.create":"Content Created","content.update":"Content Updated","content.publish":"Content Published","content.unpublish":"Content Unpublished","content.delete":"Content Deleted","content.schedule":"Content Scheduled","content.unschedule":"Content Unscheduled","content.update_publish_date":"Publish Date Changed","content.discard":"Draft Discarded","media.create":"Media Uploaded","media.update":"Media Updated","media.delete":"Media Deleted","media.bulk":"Bulk Media Operation","media.folder.create":"Folder Created","media.folder.update":"Folder Renamed","media.folder.delete":"Folder Deleted","collection.create":"Collection Created","collection.update":"Collection Updated","collection.delete":"Collection Deleted","blocktype.create":"Block Type Created","blocktype.update":"Block Type Updated","blocktype.delete":"Block Type Deleted","webhook.create":"Webhook Created","webhook.update":"Webhook Updated","webhook.delete":"Webhook Deleted","webhook.rotate_secret":"Webhook Secret Rotated","webhook.retry_delivery":"Delivery Retried","apikey.create":"API Key Created","apikey.revoke":"API Key Revoked","site.archived":"Site Archived","site.unarchived":"Site Unarchived","site.purged":"Site Purged","site_user.create":"User Created","site_user.update":"User Updated","site_user.delete":"User Deleted","site_user.change_email":"User Email Changed","site_user.self_register":"User Self-Registered","site_user.self_update":"User Updated Profile"};function ve(t){return!!t?.blockId&&!!t?.prevBlockId&&!!t?.resourceId}function Se({host:t,details:a}){const l=a.resourceId,d=a.collection??"",{data:o}=H(t,d),{data:s,isLoading:b}=T(t,l,a.blockId??null),{data:n,isLoading:u}=T(t,l,a.prevBlockId??null),c=i.useMemo(()=>o?.fields??[],[o?.fields]),g=i.useMemo(()=>{if(!s)return[];const h={title:s.title,slug:s.slug,snippet:s.snippet,tags:s.tags};return _(c,s.body??{},h)},[s,c]),j=i.useMemo(()=>{if(!n)return[];const h={title:n.title,slug:n.slug,snippet:n.snippet,tags:n.tags};return _(c,n.body??{},h)},[n,c]);return b||u?e.jsxs("div",{className:"space-y-2","data-testid":"diff-loading",children:[e.jsx(y,{className:"h-6 w-24"}),e.jsx(y,{className:"h-40 w-full"})]}):!s||!n?e.jsx("p",{className:"text-muted-foreground text-sm","data-testid":"diff-unavailable",children:"Version data unavailable."}):e.jsxs("div",{"data-testid":"diff-view",children:[e.jsx("h3",{className:"mb-2 text-sm font-medium",children:"Changes"}),e.jsx(he,{left:j,right:g,leftLabel:"Before",rightLabel:"After"})]})}function Ae({open:t,onOpenChange:a,event:l,host:d,resolveAdmin:o}){if(!l)return null;const s=l.details,b=Ne[l.action]??l.action,n=ve(s),u=s?.resourceType==="content"&&s?.resourceId&&s?.collection?`/sites/${d}/content/${s.collection}/${s.resourceId}`:null;return e.jsx(K,{open:t,onOpenChange:a,children:e.jsxs(V,{className:"w-full overflow-y-auto px-6 sm:max-w-lg","data-testid":"audit-drawer",children:[e.jsx(W,{children:e.jsx(q,{children:b})}),e.jsxs("div",{className:"mt-4 space-y-4",children:[e.jsxs("dl",{className:"grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm",children:[s?.title&&e.jsxs(e.Fragment,{children:[e.jsx("dt",{className:"text-muted-foreground",children:"Resource"}),e.jsx("dd",{className:"font-medium","data-testid":"drawer-resource",children:s.title})]}),s?.collection&&e.jsxs(e.Fragment,{children:[e.jsx("dt",{className:"text-muted-foreground",children:"Collection"}),e.jsx("dd",{children:s.collection})]}),e.jsx("dt",{className:"text-muted-foreground",children:"Admin"}),e.jsx("dd",{children:o(l.adminId)}),e.jsx("dt",{className:"text-muted-foreground",children:"Time"}),e.jsx("dd",{children:P(l.createdAt)}),e.jsx("dt",{className:"text-muted-foreground",children:"Status"}),e.jsx("dd",{children:e.jsx("span",{className:l.status==="success"?"text-green-600":l.status==="failed"?"text-red-600":"text-yellow-600",children:l.status})}),s?.ipAddress&&e.jsxs(e.Fragment,{children:[e.jsx("dt",{className:"text-muted-foreground",children:"IP Address"}),e.jsx("dd",{className:"font-mono text-xs",children:s.ipAddress})]}),s?.extra&&e.jsxs(e.Fragment,{children:[e.jsx("dt",{className:"text-muted-foreground",children:"Details"}),e.jsx("dd",{className:"text-muted-foreground",children:s.extra})]})]}),s&&e.jsxs("details",{className:"text-sm","data-testid":"drawer-json",children:[e.jsx("summary",{className:"text-muted-foreground cursor-pointer select-none",children:"Raw details"}),e.jsx("pre",{className:"bg-muted mt-2 overflow-x-auto rounded p-3 text-xs",children:JSON.stringify(s,null,2)})]}),l.errorMsg&&e.jsx("div",{className:"rounded border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950 dark:text-red-300","data-testid":"drawer-error",children:l.errorMsg}),u&&e.jsx(R,{variant:"outline",size:"sm",asChild:!0,children:e.jsx(L,{to:u,children:"View content"})}),n&&s&&e.jsx("div",{className:"border-t pt-4",children:e.jsx(Se,{host:d,details:s})}),!n&&s?.resourceType==="content"&&e.jsx("p",{className:"text-muted-foreground text-sm","data-testid":"no-changes",children:"No change comparison available for this event."})]})]})})}const Ue={"content.create":{label:"Created content",Icon:A},"content.update":{label:"Updated content",Icon:m},"content.publish":{label:"Published content",Icon:ue},"content.unpublish":{label:"Unpublished content",Icon:ie},"content.delete":{label:"Deleted content",Icon:x},"content.schedule":{label:"Scheduled content",Icon:ce},"content.unschedule":{label:"Unscheduled content",Icon:oe},"content.update_publish_date":{label:"Changed publish date",Icon:ne},"content.discard":{label:"Discarded draft",Icon:de},"media.create":{label:"Uploaded media",Icon:re},"media.update":{label:"Updated media",Icon:m},"media.delete":{label:"Deleted media",Icon:x},"media.bulk":{label:"Bulk media operation",Icon:S},"media.folder.create":{label:"Created folder",Icon:U},"media.folder.update":{label:"Renamed folder",Icon:m},"media.folder.delete":{label:"Deleted folder",Icon:x},"collection.create":{label:"Created collection",Icon:U},"collection.update":{label:"Updated collection",Icon:m},"collection.delete":{label:"Deleted collection",Icon:x},"blocktype.create":{label:"Created block type",Icon:A},"blocktype.update":{label:"Updated block type",Icon:m},"blocktype.delete":{label:"Deleted block type",Icon:x},"webhook.create":{label:"Created webhook",Icon:le},"webhook.update":{label:"Updated webhook",Icon:m},"webhook.delete":{label:"Deleted webhook",Icon:x},"webhook.rotate_secret":{label:"Rotated webhook secret",Icon:ae},"webhook.retry_delivery":{label:"Retried delivery",Icon:S},"apikey.create":{label:"Created API key",Icon:se},"apikey.revoke":{label:"Revoked API key",Icon:te},"site.archived":{label:"Archived site",Icon:ee},"site.unarchived":{label:"Unarchived site",Icon:Z},"site.purged":{label:"Purged site",Icon:Y},"site_user.create":{label:"Created user",Icon:v},"site_user.update":{label:"Updated user",Icon:X},"site_user.delete":{label:"Deleted user",Icon:Q},"site_user.change_email":{label:"Changed user email",Icon:G},"site_user.self_register":{label:"User registered",Icon:v},"site_user.self_update":{label:"User updated profile",Icon:z}},_e=[{label:"All",value:"all"},{label:"Content",value:"content."},{label:"Media",value:"media."},{label:"Collections",value:"collection."},{label:"Block Types",value:"blocktype."},{label:"Webhooks",value:"webhook."},{label:"API Keys",value:"apikey."},{label:"Sites",value:"site."},{label:"Site Users",value:"site_user."}];function Te(t){return Ue[t]??{label:t,Icon:m}}function De(t,a){const l=a.resourceType??a.details?.resourceType,d=a.resourceId??a.details?.resourceId;if(!l||!d)return null;switch(l){case"content":{const o=a.details?.collection;return o?`/sites/${t}/content/${o}/${d}`:`/sites/${t}/content`}case"media":return`/sites/${t}/media`;case"collection":return`/sites/${t}/settings/collections`;case"webhook":return`/sites/${t}/settings/webhooks`;case"site_user":return`/sites/${t}/settings/site-users/${d}`;default:return null}}function Le({status:t}){const a=t==="success"?"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400":t==="failed"?"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400":"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400";return e.jsx("span",{className:`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${a}`,children:t})}function qe(){const{host:t}=E(),[a,l]=i.useState(),[d,o]=i.useState("all"),[s,b]=i.useState(null),[n,u]=i.useState(!1),{data:c,isLoading:g}=we(t,{before:a}),j=c?.items.map(r=>r.adminId)??[],{resolve:h}=me(j),$=J(),B=i.useCallback(r=>{b(r),u(!0)},[]);Ce({title:"Audit Log"});const C=d!=="all"?c?.items.filter(r=>r.action.startsWith(d)):c?.items;return e.jsxs("div",{children:[e.jsxs("div",{className:"mb-6 flex items-center justify-between gap-4",children:[!$&&e.jsx("h1",{className:"text-2xl font-semibold",children:"Audit Log"}),e.jsxs(be,{value:d,onValueChange:o,children:[e.jsx(pe,{className:"w-[180px]","data-testid":"action-filter",children:e.jsx(fe,{placeholder:"Filter by type"})}),e.jsx(ge,{children:_e.map(r=>e.jsx(je,{value:r.value,children:r.label},r.value))})]})]}),g?e.jsx("div",{className:"space-y-2",children:Array.from({length:5}).map((r,k)=>e.jsx(y,{className:"h-12 w-full"},k))}):C?.length?e.jsxs(e.Fragment,{children:[e.jsxs(ke,{children:[e.jsx(Ie,{children:e.jsxs(D,{children:[e.jsx(p,{children:"Action"}),e.jsx(p,{children:"Resource"}),e.jsx(p,{children:"Admin"}),e.jsx(p,{children:"Status"}),e.jsx(p,{children:"Time"})]})}),e.jsx(ye,{children:C.map(r=>{const{label:k,Icon:M}=Te(r.action),I=r.details?.title,w=r.details?.collection,N=t?De(t,r):null;return e.jsxs(D,{"data-testid":"audit-row",className:"cursor-pointer",onClick:()=>B(r),children:[e.jsx(f,{children:e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(M,{className:"text-muted-foreground h-4 w-4 shrink-0"}),e.jsx("span",{className:"font-medium",children:k})]})}),e.jsx(f,{children:I?e.jsxs("div",{className:"flex items-center gap-2",children:[N?e.jsx(L,{to:N,className:"text-primary hover:underline",children:I}):e.jsx("span",{children:I}),w&&e.jsx("span",{className:"bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-xs",children:w})]}):e.jsx("span",{className:"text-muted-foreground",children:"-"})}),e.jsx(f,{className:"text-sm",children:h(r.adminId)}),e.jsx(f,{children:e.jsx(Le,{status:r.status})}),e.jsx(f,{className:"text-muted-foreground",title:P(r.createdAt),children:xe(r.createdAt)})]},r.eventId)})})]}),c?.hasMore&&c.items.length>0&&e.jsx("div",{className:"mt-4 flex justify-center",children:e.jsx(R,{variant:"outline",onClick:()=>l(c.items[c.items.length-1].createdAt),children:"Load More"})})]}):e.jsx("p",{className:"text-muted-foreground",children:"No audit events."}),e.jsx(Ae,{open:n,onOpenChange:u,event:s,host:t,resolveAdmin:h})]})}export{qe as AuditPage};