headroom-cms 0.1.9 → 0.1.11

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 (154) hide show
  1. package/README.md +11 -6
  2. package/admin/assets/{AdminsPage-Bt_ekZen.js → AdminsPage-BnzH9TL3.js} +1 -1
  3. package/admin/assets/AllContentPage-BtObN6oy.js +1 -0
  4. package/admin/assets/{ApiKeysPage-BfWCxGhC.js → ApiKeysPage-DEAa8eyC.js} +1 -1
  5. package/admin/assets/AuditPage-BN9yNsxh.js +1 -0
  6. package/admin/assets/BlockEditor-3wnisTOZ.js +176 -0
  7. package/admin/assets/BlockEditor-CQpF8tYb.css +1 -0
  8. package/admin/assets/BlockTypeEditPage-C2evAESK.js +1 -0
  9. package/admin/assets/BlockTypesPage-Dhkho6T_.js +1 -0
  10. package/admin/assets/{BulkActionBar-TRiXXLQd.js → BulkActionBar-BxdfUSrN.js} +1 -1
  11. package/admin/assets/CollectionEditPage-lOb4hEZy.js +1 -0
  12. package/admin/assets/{CollectionsPage-ClplrxNn.js → CollectionsPage-CgtOloa1.js} +1 -1
  13. package/admin/assets/{ContentCreatePage-DfYcEH1u.js → ContentCreatePage-LeQjahp_.js} +1 -1
  14. package/admin/assets/ContentEditPage-xczr4d_h.js +1 -0
  15. package/admin/assets/ContentField-pilCbdnA.js +1 -0
  16. package/admin/assets/ContentListPage-BAKDn1Xy.js +1 -0
  17. package/admin/assets/CustomBlockPreview-DNnTFM0z.js +479 -0
  18. package/admin/assets/FieldRenderer-DiOKvkWV.js +2 -0
  19. package/admin/assets/FilterBar-BZoa63zh.js +1 -0
  20. package/admin/assets/FloatingComposerController-D4uLQfUX-BMIvFCoE.js +1 -0
  21. package/admin/assets/IconPicker-CpIgiQTC.js +3 -0
  22. package/admin/assets/{LoginPage-DutieANA.js → LoginPage-D9ZsGLIi.js} +1 -1
  23. package/admin/assets/MediaField-CxccCFGQ.js +1 -0
  24. package/admin/assets/MediaPage-QvMaH2YJ.js +1 -0
  25. package/admin/assets/Pagination-Df9nQ7Z0.js +1 -0
  26. package/admin/assets/RelationshipPicker-B3Ftmqxp.js +1 -0
  27. package/admin/assets/{SiteSettingsPage-BtCC3RKc.js → SiteSettingsPage-6NvH7CiQ.js} +1 -1
  28. package/admin/assets/{SiteUserEditPage-ClHmp0T-.js → SiteUserEditPage-D5VaQ1Xq.js} +1 -1
  29. package/admin/assets/SiteUsersPage-BYVduiqs.js +1 -0
  30. package/admin/assets/{SitesPage-Bw_WBN6v.js → SitesPage-rfWWE0yK.js} +1 -1
  31. package/admin/assets/{SubmissionDetailPage-DS08LGxd.js → SubmissionDetailPage-BSUR685F.js} +1 -1
  32. package/admin/assets/SubmissionEditPage-DjLXHjWU.js +1 -0
  33. package/admin/assets/SubmissionListPage-DBxNEvde.js +1 -0
  34. package/admin/assets/{TagInput-BILCaC9b.js → TagInput-57c4DG1w.js} +1 -1
  35. package/admin/assets/{TagsPage-DdeZokow.js → TagsPage-BEO5AwCv.js} +1 -1
  36. package/admin/assets/{UsersPage-B0vLxjrg.js → UsersPage-BpIRorJ1.js} +1 -1
  37. package/admin/assets/{WebhookEditPage-SlJE4d3z.js → WebhookEditPage-D5xgi56h.js} +1 -1
  38. package/admin/assets/{WebhooksPage-C6lGZLpr.js → WebhooksPage-BY7AaiGr.js} +1 -1
  39. package/admin/assets/{card-hXVtlM0q.js → card-C9hfyHXf.js} +1 -1
  40. package/admin/assets/checkbox-DVJcwUt1.js +1 -0
  41. package/admin/assets/{collapsible-B414SspL.js → collapsible-D3d29uJp.js} +1 -1
  42. package/admin/assets/{command-fvBFHye4.js → command-Bfmj0MEL.js} +1 -1
  43. package/admin/assets/contentStatus-CkPi9Dh6.js +1 -0
  44. package/admin/assets/{core.esm-B_kcYf6n.js → core.esm-DdQHdRkd.js} +2 -2
  45. package/admin/assets/index-BB9Syqw2.css +1 -0
  46. package/admin/assets/index-Ce5pmRMj.js +18 -0
  47. package/admin/assets/media-url-DdCoIedP.js +1 -0
  48. package/admin/assets/popover-CzaQYEEP.js +1 -0
  49. package/admin/assets/radix-C5ZmWuuL.js +51 -0
  50. package/admin/assets/select-CrRhFGIi.js +1 -0
  51. package/admin/assets/serializeToText-2VrsuRUh.js +2 -0
  52. package/admin/assets/{sortable.esm-QyXA6fio.js → sortable.esm-qVEMoaTg.js} +1 -1
  53. package/admin/assets/{table-DLoIbCQ5.js → table-_3bMY0_z.js} +1 -1
  54. package/admin/assets/{textarea-vSXNxwTe.js → textarea-6fq0R6VV.js} +1 -1
  55. package/admin/assets/useAdminResolver-BJNPz3OG.js +1 -0
  56. package/admin/assets/useContent-Bs7nel7C.js +1 -0
  57. package/admin/assets/useContentSearch-B3aTjuCu.js +1 -0
  58. package/admin/assets/{useMedia-e3sqWm_t.js → useMedia-ae3s_ajC.js} +1 -1
  59. package/admin/assets/usePageTitle-C1r1-C00.js +1 -0
  60. package/admin/assets/useSiteUsers-DIaqgNSp.js +1 -0
  61. package/admin/assets/{useTags-f7AVSLuj.js → useTags-B-HgMVwo.js} +1 -1
  62. package/admin/assets/{useWebhooks-BH_r8-Mo.js → useWebhooks-BvZjUJkJ.js} +1 -1
  63. package/admin/assets/yjs-tXBm_srz.js +5 -0
  64. package/admin/favicon-16x16.png +0 -0
  65. package/admin/favicon-32x32.png +0 -0
  66. package/admin/icons/icon-180x180.png +0 -0
  67. package/admin/icons/icon-192x192.png +0 -0
  68. package/admin/icons/icon-512x512.png +0 -0
  69. package/admin/icons/maskable-icon-512x512.png +0 -0
  70. package/admin/index.html +3 -3
  71. package/admin/sw.js +1 -1
  72. package/dist/admin-site.d.ts +16 -2
  73. package/dist/admin-site.d.ts.map +1 -1
  74. package/dist/admin-site.js +10 -3
  75. package/dist/admin-site.js.map +1 -1
  76. package/dist/api.d.ts +7 -0
  77. package/dist/api.d.ts.map +1 -1
  78. package/dist/api.js +8 -1
  79. package/dist/api.js.map +1 -1
  80. package/dist/cdn-api.d.ts +25 -0
  81. package/dist/cdn-api.d.ts.map +1 -0
  82. package/dist/{cdn.js → cdn-api.js} +7 -139
  83. package/dist/cdn-api.js.map +1 -0
  84. package/dist/cdn-media.d.ts +26 -0
  85. package/dist/cdn-media.d.ts.map +1 -0
  86. package/dist/cdn-media.js +202 -0
  87. package/dist/cdn-media.js.map +1 -0
  88. package/dist/collaboration.d.ts +55 -0
  89. package/dist/collaboration.d.ts.map +1 -0
  90. package/dist/collaboration.js +141 -0
  91. package/dist/collaboration.js.map +1 -0
  92. package/dist/index.d.ts +27 -3
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +47 -12
  95. package/dist/index.js.map +1 -1
  96. package/dist/storage.d.ts +2 -0
  97. package/dist/storage.d.ts.map +1 -1
  98. package/dist/storage.js +33 -0
  99. package/dist/storage.js.map +1 -1
  100. package/lambda/api/bootstrap +0 -0
  101. package/lambda/image-lambda/node_modules/.package-lock.json +3 -3
  102. package/lambda/image-lambda/node_modules/semver/README.md +19 -4
  103. package/lambda/image-lambda/node_modules/semver/bin/semver.js +14 -10
  104. package/lambda/image-lambda/node_modules/semver/functions/truncate.js +48 -0
  105. package/lambda/image-lambda/node_modules/semver/index.js +2 -0
  106. package/lambda/image-lambda/node_modules/semver/internal/re.js +1 -1
  107. package/lambda/image-lambda/node_modules/semver/package.json +3 -3
  108. package/lambda/image-lambda/node_modules/semver/range.bnf +5 -4
  109. package/lambda/webhook-worker/bootstrap +0 -0
  110. package/package.json +1 -1
  111. package/src/admin-site.ts +26 -5
  112. package/src/api.ts +15 -1
  113. package/src/{cdn.ts → cdn-api.ts} +8 -161
  114. package/src/cdn-media.ts +250 -0
  115. package/src/collaboration.ts +187 -0
  116. package/src/index.ts +77 -14
  117. package/src/sst-env.d.ts +28 -0
  118. package/src/storage.ts +35 -0
  119. package/admin/assets/AllContentPage-CFqEMAl9.js +0 -1
  120. package/admin/assets/AuditPage-BE0XIUl2.js +0 -1
  121. package/admin/assets/BlockEditor-6wqsThJ7.js +0 -179
  122. package/admin/assets/BlockEditor-Cp_wZ2xN.css +0 -1
  123. package/admin/assets/BlockTypeEditPage-CuNJfZw0.js +0 -1
  124. package/admin/assets/BlockTypesPage-BIMBVxBs.js +0 -1
  125. package/admin/assets/CollectionEditPage-BqX_0cC2.js +0 -1
  126. package/admin/assets/ContentEditPage-D3Rvlktk.js +0 -2
  127. package/admin/assets/ContentListPage-zmO8Is4d.js +0 -1
  128. package/admin/assets/CustomBlockPreview-C6HqS4xv.js +0 -479
  129. package/admin/assets/FieldBuilder-36tfpSyM.js +0 -3
  130. package/admin/assets/FilterBar-DhRwTqFv.js +0 -1
  131. package/admin/assets/MediaField-J2TLG_fu.js +0 -1
  132. package/admin/assets/MediaPage-DZZKMGF4.js +0 -1
  133. package/admin/assets/RelationshipPicker-CDFs4TMW.js +0 -1
  134. package/admin/assets/SiteUsersPage-AyJvcVM7.js +0 -1
  135. package/admin/assets/SubmissionEditPage-Brf-DK2X.js +0 -1
  136. package/admin/assets/SubmissionListPage-DNMzQZHS.js +0 -1
  137. package/admin/assets/checkbox-WGrS3sUr.js +0 -1
  138. package/admin/assets/contentStatus-BmaiYVOm.js +0 -1
  139. package/admin/assets/index-Cir9tY_P.js +0 -18
  140. package/admin/assets/index-DACBYsKM.css +0 -1
  141. package/admin/assets/media-url-DIg_vSyf.js +0 -1
  142. package/admin/assets/popover-D5_HjjUC.js +0 -1
  143. package/admin/assets/radix-C1kb_NqW.js +0 -51
  144. package/admin/assets/select-_uJYxzeZ.js +0 -1
  145. package/admin/assets/serializeToText-DR_WnxiI.js +0 -2
  146. package/admin/assets/useAdminResolver-D-LlmquD.js +0 -1
  147. package/admin/assets/useContent-e8beBIuq.js +0 -1
  148. package/admin/assets/useContentSearch-DOjveB9t.js +0 -1
  149. package/admin/assets/useDebouncedValue-C-cQUcLG.js +0 -1
  150. package/admin/assets/usePageTitle-BNSba9_L.js +0 -1
  151. package/admin/assets/useSiteUsers-BdnvuM2E.js +0 -1
  152. package/dist/cdn.d.ts +0 -27
  153. package/dist/cdn.d.ts.map +0 -1
  154. package/dist/cdn.js.map +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
@@ -12,8 +12,10 @@ import { createAuth } from "./auth.js";
12
12
  import { createWebhooks } from "./webhooks.js";
13
13
  import { createImage } from "./image.js";
14
14
  import { createApi } from "./api.js";
15
- import { createCdn } from "./cdn.js";
15
+ import { createApiCdn } from "./cdn-api.js";
16
+ import { createMediaCdn } from "./cdn-media.js";
16
17
  import { createScheduler } from "./scheduler.js";
18
+ import { createCollabTable, createCollabHandler } from "./collaboration.js";
17
19
  import { createAdminSite } from "./admin-site.js";
18
20
 
19
21
  /**
@@ -48,8 +50,10 @@ export type { AuthResources } from "./auth.js";
48
50
  export type { WebhookResources } from "./webhooks.js";
49
51
  export type { ImageResources } from "./image.js";
50
52
  export type { ApiResources } from "./api.js";
51
- export type { CdnResources } from "./cdn.js";
53
+ export type { ApiCdnResources } from "./cdn-api.js";
54
+ export type { MediaCdnResources } from "./cdn-media.js";
52
55
  export type { SchedulerResources } from "./scheduler.js";
56
+ export type { CollaborationResources } from "./collaboration.js";
53
57
  export type { AdminSiteResources } from "./admin-site.js";
54
58
 
55
59
  export interface HeadroomCMSArgs {
@@ -61,7 +65,7 @@ export interface HeadroomCMSArgs {
61
65
  senderEmail: string;
62
66
 
63
67
  /**
64
- * Custom domain for the CDN (API endpoint).
68
+ * Custom domain for the API CDN (`/v1/*` and `/health`).
65
69
  * If not provided, uses the default CloudFront domain.
66
70
  */
67
71
  domain?: {
@@ -70,6 +74,17 @@ export interface HeadroomCMSArgs {
70
74
  certificateArn: string;
71
75
  };
72
76
 
77
+ /**
78
+ * Custom domain for the media CDN (S3 + image-Lambda origin, serves
79
+ * `/media/*` and `/img/*`). If not provided, uses the default CloudFront
80
+ * domain. The certificate must be in us-east-1, same as `domain`.
81
+ */
82
+ mediaDomain?: {
83
+ name: string;
84
+ /** ACM certificate ARN (must be in us-east-1 for CloudFront) */
85
+ certificateArn: string;
86
+ };
87
+
73
88
  /**
74
89
  * Custom domain for the admin UI.
75
90
  * If not provided, uses the default CloudFront domain.
@@ -120,15 +135,28 @@ export interface HeadroomCMSArgs {
120
135
  adminPath: string;
121
136
  /** Go source path for scheduler Lambda, e.g. "packages/scheduler" */
122
137
  schedulerHandler?: string;
138
+ /** SST handler for collab Lambda, e.g. "packages/collab/src/index.handler" */
139
+ collabHandler?: string;
123
140
  };
141
+
142
+ /**
143
+ * Build-time toggle for the collaboration feature in the admin UI.
144
+ * Defaults to `true`. Set `false` to keep the WebSocket / Collab table
145
+ * deployed but hide the "Collaborate" / "Request collaboration" buttons
146
+ * and refuse to open WebSockets — a kill-switch without teardown. Used
147
+ * to ship the feature to a stage before turning it on for editors.
148
+ */
149
+ collabEnabled?: boolean;
124
150
  }
125
151
 
126
152
  export class HeadroomCMS {
127
153
  public readonly apiUrl: $util.Output<string>;
128
- public readonly cdnUrl: $util.Output<string>;
154
+ public readonly apiCdnUrl: $util.Output<string>;
155
+ public readonly mediaCdnUrl: $util.Output<string>;
129
156
  public readonly adminUrl: $util.Output<string>;
130
157
  public readonly userPoolId: $util.Output<string>;
131
158
  public readonly userPoolClientId: $util.Output<string>;
159
+ public readonly collabWsUrl: $util.Output<string>;
132
160
 
133
161
  public readonly outputs: Record<string, $util.Output<string>>;
134
162
 
@@ -167,12 +195,17 @@ export class HeadroomCMS {
167
195
  : undefined,
168
196
  });
169
197
 
170
- // 5. API Lambda (Go handler with access to all resources)
198
+ // 5a. Collaboration table (must exist before the API Lambda, which
199
+ // links it for ticket reads/writes).
200
+ const collabTable = createCollabTable(name);
201
+
202
+ // 6. API Lambda (Go handler with access to all resources)
171
203
  const api = createApi(name, {
172
204
  storage,
173
205
  auth,
174
206
  webhooks,
175
207
  image,
208
+ collab: collabTable,
176
209
  senderEmail: args.senderEmail,
177
210
  pkgRoot,
178
211
  dev: args.dev
@@ -180,7 +213,24 @@ export class HeadroomCMS {
180
213
  : undefined,
181
214
  });
182
215
 
183
- // 6. Scheduler (EventBridge cron + Go Lambda + lock table)
216
+ // 5b. Collaboration WebSocket handler (must be created AFTER the API
217
+ // because its Lambda environment includes API_URL = api.api.url, which
218
+ // `snapshotToDraft` uses to PUT the disconnect snapshot back to the
219
+ // Go draft endpoint).
220
+ const collabHandler = createCollabHandler(name, {
221
+ storage,
222
+ auth,
223
+ api,
224
+ collabTable: collabTable.collabTable,
225
+ pkgRoot,
226
+ dev: args.dev?.collabHandler
227
+ ? { handler: args.dev.collabHandler }
228
+ : undefined,
229
+ });
230
+
231
+ const collab = { ...collabTable, ...collabHandler };
232
+
233
+ // 7. Scheduler (EventBridge cron + Go Lambda + lock table)
184
234
  const schedulerResources = createScheduler(name, {
185
235
  storage,
186
236
  api,
@@ -190,42 +240,55 @@ export class HeadroomCMS {
190
240
  : undefined,
191
241
  });
192
242
 
193
- // 7. CDN (CloudFront distribution + edge functions)
194
- const cdn = createCdn(name, {
243
+ // 8a. API CDN (CloudFront distribution + edge auth, /v1/* and /health)
244
+ const apiCdn = createApiCdn(name, {
195
245
  api,
196
- image,
197
- contentBucket: storage.contentBucket,
198
246
  kvs: storage.kvs,
199
247
  priceClass: args.priceClass,
200
248
  apiCacheTtl: args.apiCacheTtl,
201
249
  domain: args.domain,
202
250
  });
203
251
 
204
- // 8. Admin UI (static site)
252
+ // 8b. Media CDN (CloudFront distribution, /media/* and /img/* only)
253
+ const mediaCdn = createMediaCdn(name, {
254
+ image,
255
+ contentBucket: storage.contentBucket,
256
+ priceClass: args.priceClass,
257
+ domain: args.mediaDomain,
258
+ });
259
+
260
+ // 9. Admin UI (static site)
205
261
  const admin = createAdminSite(name, {
206
262
  api,
207
- cdn,
263
+ apiCdn,
264
+ mediaCdn,
208
265
  auth,
266
+ collab,
209
267
  pkgRoot,
210
268
  dev: args.dev
211
269
  ? { adminPath: args.dev.adminPath }
212
270
  : undefined,
213
271
  domain: args.adminDomain,
272
+ collabEnabled: args.collabEnabled,
214
273
  });
215
274
 
216
275
  // Expose outputs
217
276
  this.apiUrl = api.api.url;
218
- this.cdnUrl = cdn.url;
277
+ this.apiCdnUrl = apiCdn.url;
278
+ this.mediaCdnUrl = mediaCdn.url;
219
279
  this.adminUrl = admin.url;
220
280
  this.userPoolId = auth.userPool.id;
221
281
  this.userPoolClientId = auth.userPoolClient.id;
282
+ this.collabWsUrl = collab.wsUrl;
222
283
 
223
284
  this.outputs = {
224
285
  api: api.api.url,
225
- cdn: cdn.url,
286
+ apiCdn: apiCdn.url,
287
+ mediaCdn: mediaCdn.url,
226
288
  admin: admin.url,
227
289
  userPoolId: auth.userPool.id,
228
290
  userPoolClientId: auth.userPoolClient.id,
291
+ collabWs: collab.wsUrl,
229
292
  };
230
293
  }
231
294
  }
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};