@agent-native/creative-context 0.3.0 → 0.5.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 (185) hide show
  1. package/README.md +51 -9
  2. package/agent-native.package.json +4 -0
  3. package/dist/actions/index.d.ts.map +1 -1
  4. package/dist/actions/index.js +8 -0
  5. package/dist/actions/index.js.map +1 -1
  6. package/dist/actions/list-context-memberships.d.ts +11 -0
  7. package/dist/actions/list-context-memberships.d.ts.map +1 -0
  8. package/dist/actions/list-context-memberships.js +47 -0
  9. package/dist/actions/list-context-memberships.js.map +1 -0
  10. package/dist/actions/list-creative-contexts.d.ts +12 -0
  11. package/dist/actions/list-creative-contexts.d.ts.map +1 -0
  12. package/dist/actions/list-creative-contexts.js +28 -0
  13. package/dist/actions/list-creative-contexts.js.map +1 -0
  14. package/dist/actions/manage-context-membership.d.ts +64 -0
  15. package/dist/actions/manage-context-membership.d.ts.map +1 -0
  16. package/dist/actions/manage-context-membership.js +70 -0
  17. package/dist/actions/manage-context-membership.js.map +1 -0
  18. package/dist/actions/manage-context-pack.d.ts +3 -0
  19. package/dist/actions/manage-context-pack.js +6 -0
  20. package/dist/actions/manage-context-pack.js.map +1 -1
  21. package/dist/actions/manage-context-source.js +4 -11
  22. package/dist/actions/manage-context-source.js.map +1 -1
  23. package/dist/actions/manage-creative-context.d.ts +28 -0
  24. package/dist/actions/manage-creative-context.d.ts.map +1 -0
  25. package/dist/actions/manage-creative-context.js +65 -0
  26. package/dist/actions/manage-creative-context.js.map +1 -0
  27. package/dist/actions/process-context-purge.d.ts +16 -1
  28. package/dist/actions/process-context-purge.js +2 -1
  29. package/dist/actions/process-context-purge.js.map +1 -1
  30. package/dist/actions/search-creative-context.d.ts +1 -0
  31. package/dist/actions/search-creative-context.js +2 -1
  32. package/dist/actions/search-creative-context.js.map +1 -1
  33. package/dist/client/CreativeContextChip.d.ts +5 -1
  34. package/dist/client/CreativeContextChip.d.ts.map +1 -1
  35. package/dist/client/CreativeContextChip.js +36 -7
  36. package/dist/client/CreativeContextChip.js.map +1 -1
  37. package/dist/client/CreativeContextPanel.d.ts.map +1 -1
  38. package/dist/client/CreativeContextPanel.js +406 -89
  39. package/dist/client/CreativeContextPanel.js.map +1 -1
  40. package/dist/client/CreativeContextShareTab.d.ts +56 -0
  41. package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
  42. package/dist/client/CreativeContextShareTab.js +213 -0
  43. package/dist/client/CreativeContextShareTab.js.map +1 -0
  44. package/dist/client/actions.d.ts +210 -0
  45. package/dist/client/actions.d.ts.map +1 -1
  46. package/dist/client/actions.js +318 -0
  47. package/dist/client/actions.js.map +1 -1
  48. package/dist/client/application-state.d.ts +2 -0
  49. package/dist/client/application-state.d.ts.map +1 -1
  50. package/dist/client/application-state.js +25 -2
  51. package/dist/client/application-state.js.map +1 -1
  52. package/dist/client/index.d.ts +4 -3
  53. package/dist/client/index.d.ts.map +1 -1
  54. package/dist/client/index.js +4 -3
  55. package/dist/client/index.js.map +1 -1
  56. package/dist/client/messages.d.ts +1 -1
  57. package/dist/client/messages.d.ts.map +1 -1
  58. package/dist/client/messages.js +77 -0
  59. package/dist/client/messages.js.map +1 -1
  60. package/dist/connectors/types.d.ts +2 -2
  61. package/dist/connectors/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +1592 -0
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +95 -0
  65. package/dist/schema/index.js.map +1 -1
  66. package/dist/schema/migrations.d.ts.map +1 -1
  67. package/dist/schema/migrations.js +80 -0
  68. package/dist/schema/migrations.js.map +1 -1
  69. package/dist/server/generation-artifact-access.js +2 -0
  70. package/dist/server/generation-artifact-access.js.map +1 -1
  71. package/dist/server/generation-context.d.ts +12 -4
  72. package/dist/server/generation-context.d.ts.map +1 -1
  73. package/dist/server/generation-context.js +129 -7
  74. package/dist/server/generation-context.js.map +1 -1
  75. package/dist/server/index.d.ts +4 -0
  76. package/dist/server/index.d.ts.map +1 -1
  77. package/dist/server/index.js +16 -0
  78. package/dist/server/index.js.map +1 -1
  79. package/dist/server/isolated-a2a.d.ts +3 -0
  80. package/dist/server/isolated-a2a.d.ts.map +1 -1
  81. package/dist/server/isolated-a2a.js +3 -1
  82. package/dist/server/isolated-a2a.js.map +1 -1
  83. package/dist/server/media.d.ts +1 -1
  84. package/dist/server/media.d.ts.map +1 -1
  85. package/dist/server/media.js +24 -9
  86. package/dist/server/media.js.map +1 -1
  87. package/dist/server/native-resource-capture.d.ts +78 -0
  88. package/dist/server/native-resource-capture.d.ts.map +1 -0
  89. package/dist/server/native-resource-capture.js +94 -0
  90. package/dist/server/native-resource-capture.js.map +1 -0
  91. package/dist/server/public-serialization.js +1 -1
  92. package/dist/server/public-serialization.js.map +1 -1
  93. package/dist/server/retrieval.d.ts +1 -0
  94. package/dist/server/retrieval.d.ts.map +1 -1
  95. package/dist/server/retrieval.js +2 -0
  96. package/dist/server/retrieval.js.map +1 -1
  97. package/dist/server/safe-native-preview.d.ts +15 -0
  98. package/dist/server/safe-native-preview.d.ts.map +1 -0
  99. package/dist/server/safe-native-preview.js +149 -0
  100. package/dist/server/safe-native-preview.js.map +1 -0
  101. package/dist/store/brand.d.ts.map +1 -1
  102. package/dist/store/brand.js +2 -16
  103. package/dist/store/brand.js.map +1 -1
  104. package/dist/store/content.d.ts +1 -0
  105. package/dist/store/content.d.ts.map +1 -1
  106. package/dist/store/content.js +84 -20
  107. package/dist/store/content.js.map +1 -1
  108. package/dist/store/contexts.d.ts +216 -0
  109. package/dist/store/contexts.d.ts.map +1 -0
  110. package/dist/store/contexts.js +1341 -0
  111. package/dist/store/contexts.js.map +1 -0
  112. package/dist/store/index.d.ts +1 -0
  113. package/dist/store/index.d.ts.map +1 -1
  114. package/dist/store/index.js +1 -0
  115. package/dist/store/index.js.map +1 -1
  116. package/dist/store/packs.d.ts +3 -0
  117. package/dist/store/packs.d.ts.map +1 -1
  118. package/dist/store/packs.js +126 -9
  119. package/dist/store/packs.js.map +1 -1
  120. package/dist/store/purge.d.ts.map +1 -1
  121. package/dist/store/purge.js +103 -6
  122. package/dist/store/purge.js.map +1 -1
  123. package/dist/store/sources.d.ts.map +1 -1
  124. package/dist/store/sources.js +2 -15
  125. package/dist/store/sources.js.map +1 -1
  126. package/dist/store/suggestions.d.ts.map +1 -1
  127. package/dist/store/suggestions.js +3 -1
  128. package/dist/store/suggestions.js.map +1 -1
  129. package/dist/types.d.ts +77 -0
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/types.js +9 -1
  132. package/dist/types.js.map +1 -1
  133. package/docs/skills/creative-context/SKILL.md +13 -5
  134. package/package.json +3 -3
  135. package/src/actions/index.ts +8 -0
  136. package/src/actions/job-public-serialization.spec.ts +21 -0
  137. package/src/actions/list-context-memberships.ts +56 -0
  138. package/src/actions/list-creative-contexts.spec.ts +38 -0
  139. package/src/actions/list-creative-contexts.ts +32 -0
  140. package/src/actions/manage-context-membership.ts +80 -0
  141. package/src/actions/manage-context-pack.ts +6 -0
  142. package/src/actions/manage-context-source.ts +8 -11
  143. package/src/actions/manage-creative-context.ts +73 -0
  144. package/src/actions/process-context-purge.ts +4 -1
  145. package/src/actions/search-creative-context.ts +2 -1
  146. package/src/client/CreativeContextChip.spec.ts +32 -0
  147. package/src/client/CreativeContextChip.tsx +83 -14
  148. package/src/client/CreativeContextPanel.tsx +1947 -885
  149. package/src/client/CreativeContextShareTab.spec.ts +70 -0
  150. package/src/client/CreativeContextShareTab.tsx +657 -0
  151. package/src/client/actions.spec.ts +177 -0
  152. package/src/client/actions.ts +566 -0
  153. package/src/client/application-state.spec.ts +5 -0
  154. package/src/client/application-state.ts +30 -2
  155. package/src/client/index.ts +33 -0
  156. package/src/client/messages.ts +95 -0
  157. package/src/connectors/types.ts +6 -8
  158. package/src/eval/library-flow.acceptance.spec.tsx +1 -0
  159. package/src/schema/index.ts +116 -0
  160. package/src/schema/migrations.ts +80 -0
  161. package/src/server/generation-artifact-access.ts +1 -0
  162. package/src/server/generation-context-a2a.spec.ts +133 -0
  163. package/src/server/generation-context.ts +153 -7
  164. package/src/server/index.spec.ts +15 -0
  165. package/src/server/index.ts +16 -0
  166. package/src/server/isolated-a2a.ts +4 -1
  167. package/src/server/media.spec.ts +30 -2
  168. package/src/server/media.ts +31 -12
  169. package/src/server/native-resource-capture.spec.ts +121 -0
  170. package/src/server/native-resource-capture.ts +198 -0
  171. package/src/server/public-serialization.spec.ts +1 -0
  172. package/src/server/public-serialization.ts +1 -1
  173. package/src/server/retrieval.ts +3 -0
  174. package/src/server/safe-native-preview.spec.ts +29 -0
  175. package/src/server/safe-native-preview.ts +200 -0
  176. package/src/store/access.integration.spec.ts +1116 -0
  177. package/src/store/brand.ts +2 -21
  178. package/src/store/content.ts +217 -17
  179. package/src/store/contexts.ts +1826 -0
  180. package/src/store/index.ts +1 -0
  181. package/src/store/packs.ts +279 -24
  182. package/src/store/purge.ts +139 -6
  183. package/src/store/sources.ts +2 -18
  184. package/src/store/suggestions.ts +3 -1
  185. package/src/types.ts +95 -0
@@ -0,0 +1,1341 @@
1
+ import { assertCurrentRequestUserIsOrgAdmin, currentRequestUserIsOrgAdmin, } from "@agent-native/core/server";
2
+ import { accessFilter, assertAccess, resolveAccess, } from "@agent-native/core/sharing";
3
+ import { and, asc, count, eq, gt, inArray, isNotNull, isNull, or, } from "drizzle-orm";
4
+ import { creativeContextMediaUrl } from "../media-url.js";
5
+ import { getCreativeContext } from "../server/context.js";
6
+ import { captureNativeCreativeResource, parseNativeCreativeArtifactKey, resolveNativeCreativeResourceUpdateStatuses, } from "../server/native-resource-capture.js";
7
+ import { sanitizePublicMetadata } from "../server/public-serialization.js";
8
+ import { getCreativeContextItem } from "./content.js";
9
+ import { newId, nowIso, parseJson, requireActor, stringifyJson, } from "./helpers.js";
10
+ function defaultContextScopeKey(actor) {
11
+ return actor.orgId ? `org:${actor.orgId}` : `user:${actor.ownerEmail}`;
12
+ }
13
+ function mapContext(row, memberCount = 0, role = "viewer") {
14
+ return {
15
+ id: row.id,
16
+ name: row.name,
17
+ description: row.description ?? null,
18
+ kind: row.kind,
19
+ brandProfileId: row.brandProfileId ?? null,
20
+ approvalPolicy: row.approvalPolicy,
21
+ archivedAt: row.archivedAt ?? null,
22
+ visibility: row.visibility,
23
+ memberCount,
24
+ createdAt: row.createdAt,
25
+ updatedAt: row.updatedAt,
26
+ access: {
27
+ role,
28
+ canSubmit: role !== "viewer",
29
+ canReview: role === "owner" || role === "admin",
30
+ canAdmin: role === "owner" || role === "admin",
31
+ },
32
+ };
33
+ }
34
+ function mapMembership(row) {
35
+ return {
36
+ id: row.id,
37
+ contextId: row.contextId,
38
+ artifactKey: row.artifactKey,
39
+ publishedItemId: row.publishedItemId ?? null,
40
+ publishedItemVersionId: row.publishedItemVersionId ?? null,
41
+ pendingSubmissionId: row.pendingSubmissionId ?? null,
42
+ rank: row.rank,
43
+ purpose: row.purpose ?? null,
44
+ status: row.status,
45
+ createdAt: row.createdAt,
46
+ updatedAt: row.updatedAt,
47
+ };
48
+ }
49
+ function mapSubmission(row) {
50
+ return {
51
+ id: row.id,
52
+ contextId: row.contextId,
53
+ membershipId: row.membershipId,
54
+ artifactKey: row.artifactKey,
55
+ publishedItemId: row.publishedItemId ?? null,
56
+ publishedItemVersionId: row.publishedItemVersionId ?? null,
57
+ note: row.note ?? null,
58
+ status: row.status,
59
+ submittedBy: row.submittedBy,
60
+ reviewedBy: row.reviewedBy ?? null,
61
+ reviewNote: row.reviewNote ?? null,
62
+ createdAt: row.createdAt,
63
+ reviewedAt: row.reviewedAt ?? null,
64
+ };
65
+ }
66
+ async function appendAudit(tx, contextId, operation, details) {
67
+ const { schema } = getCreativeContext();
68
+ const actor = requireActor();
69
+ await tx.insert(schema.creativeContextAudit).values({
70
+ id: newId("cca"),
71
+ contextId,
72
+ operation,
73
+ actorEmail: actor.ownerEmail,
74
+ details: stringifyJson(details),
75
+ createdAt: nowIso(),
76
+ ownerEmail: actor.ownerEmail,
77
+ orgId: actor.orgId,
78
+ });
79
+ }
80
+ async function assertContextRole(contextId, role) {
81
+ return assertAccess("creative-context", contextId, role, undefined, {
82
+ skipResourceBody: true,
83
+ });
84
+ }
85
+ async function requireReviewer(contextId) {
86
+ await assertContextRole(contextId, "admin");
87
+ }
88
+ function normalizedFromDetail(detail) {
89
+ return {
90
+ externalId: detail.item.externalId,
91
+ kind: detail.item.kind,
92
+ title: detail.version.title,
93
+ canonicalUrl: detail.item.canonicalUrl ?? undefined,
94
+ mimeType: detail.version.mimeType ?? undefined,
95
+ content: detail.version.content,
96
+ summary: detail.version.summary ?? undefined,
97
+ contentHash: detail.version.contentHash,
98
+ sourceModifiedAt: detail.version.sourceModifiedAt ?? undefined,
99
+ sourceVersion: detail.version.sourceVersion ?? undefined,
100
+ rawSnapshotBlobRef: detail.version.rawSnapshotBlobRef ?? undefined,
101
+ parseStatus: detail.version.parseStatus,
102
+ parseError: detail.version.parseError ?? undefined,
103
+ upstreamAccess: detail.item.upstreamAccess,
104
+ curationStatus: "included",
105
+ curationRank: detail.item.curationRank === "ignored"
106
+ ? "normal"
107
+ : detail.item.curationRank,
108
+ tags: detail.item.tags,
109
+ colors: detail.item.colors,
110
+ provenance: detail.item.provenance,
111
+ thumbnailBlobRef: detail.item.thumbnailBlobRef ?? undefined,
112
+ // Keep compiler/reassembly manifests while stripping capability-like metadata.
113
+ metadata: (sanitizePublicMetadata(detail.version.metadata) ?? {}),
114
+ chunks: detail.chunks.map((chunk) => ({
115
+ ordinal: chunk.ordinal,
116
+ kind: chunk.kind,
117
+ text: chunk.text,
118
+ ...(chunk.startOffset === null ? {} : { startOffset: chunk.startOffset }),
119
+ ...(chunk.endOffset === null ? {} : { endOffset: chunk.endOffset }),
120
+ ...(chunk.tokenCount === null ? {} : { tokenCount: chunk.tokenCount }),
121
+ metadata: chunk.metadata,
122
+ })),
123
+ media: detail.media.map((media) => ({
124
+ kind: media.kind,
125
+ ...(media.mimeType ? { mimeType: media.mimeType } : {}),
126
+ accessMode: media.accessMode,
127
+ ...(media.url ? { url: media.url } : {}),
128
+ ...(media.storageKey ? { storageKey: media.storageKey } : {}),
129
+ ...(media.provenanceUrl ? { provenanceUrl: media.provenanceUrl } : {}),
130
+ ...(media.altText ? { altText: media.altText } : {}),
131
+ ...(media.caption ? { caption: media.caption } : {}),
132
+ captionStatus: media.captionStatus,
133
+ ...(media.ocrText ? { ocrText: media.ocrText } : {}),
134
+ palette: media.palette,
135
+ ...(media.contentHash ? { contentHash: media.contentHash } : {}),
136
+ ...(media.width === null ? {} : { width: media.width }),
137
+ ...(media.height === null ? {} : { height: media.height }),
138
+ ...(media.durationMs === null ? {} : { durationMs: media.durationMs }),
139
+ metadata: media.metadata,
140
+ })),
141
+ edges: detail.edges.map((edge) => ({
142
+ relation: edge.relation,
143
+ ...(edge.toItemId ? { toItemId: edge.toItemId } : {}),
144
+ ...(edge.toItemVersionId
145
+ ? { toItemVersionId: edge.toItemVersionId }
146
+ : {}),
147
+ ...(edge.toExternalId ? { toExternalId: edge.toExternalId } : {}),
148
+ metadata: edge.metadata,
149
+ })),
150
+ };
151
+ }
152
+ async function writeSnapshot(input) {
153
+ const { getDb, schema } = getCreativeContext();
154
+ const timestamp = nowIso();
155
+ const existing = await getDb()
156
+ .select()
157
+ .from(schema.contextItems)
158
+ .where(and(eq(schema.contextItems.sourceId, input.sourceId), eq(schema.contextItems.externalId, input.artifactKey)))
159
+ .limit(1);
160
+ const previous = existing[0];
161
+ const itemId = previous?.id ?? newId("cci");
162
+ const itemVersionId = newId("ccv");
163
+ const versionNumber = previous
164
+ ? (await getDb()
165
+ .select({ versionNumber: schema.contextItemVersions.versionNumber })
166
+ .from(schema.contextItemVersions)
167
+ .where(eq(schema.contextItemVersions.id, previous.currentVersionId))
168
+ .limit(1))[0]?.versionNumber + 1
169
+ : 1;
170
+ const itemValues = {
171
+ kind: input.item.kind,
172
+ title: input.item.title,
173
+ canonicalUrl: input.item.canonicalUrl ?? null,
174
+ mimeType: input.item.mimeType ?? null,
175
+ currentVersionId: itemVersionId,
176
+ currentContentHash: input.item.contentHash,
177
+ status: "active",
178
+ upstreamAccess: input.item.upstreamAccess ?? "available",
179
+ curationStatus: "included",
180
+ curationRank: input.item.curationRank ?? "normal",
181
+ starred: input.item.starred ? 1 : 0,
182
+ inventoryState: input.item.inventoryState ?? "available",
183
+ indexState: input.item.indexState ?? "pending",
184
+ tags: stringifyJson(input.item.tags ?? []),
185
+ colors: stringifyJson(input.item.colors ?? (input.item.color ? [input.item.color] : [])),
186
+ sortOrder: input.item.sortOrder ?? 0,
187
+ parentItemId: input.item.parentItemId ?? null,
188
+ provenance: stringifyJson(input.item.provenance),
189
+ thumbnailBlobRef: input.item.thumbnailBlobRef ?? null,
190
+ metadata: stringifyJson(input.item.metadata),
191
+ updatedAt: timestamp,
192
+ };
193
+ await getDb().transaction(async (tx) => {
194
+ if (previous)
195
+ await tx
196
+ .update(schema.contextItems)
197
+ .set(itemValues)
198
+ .where(eq(schema.contextItems.id, itemId));
199
+ else
200
+ await tx.insert(schema.contextItems).values({
201
+ id: itemId,
202
+ sourceId: input.sourceId,
203
+ externalId: input.artifactKey,
204
+ ...itemValues,
205
+ createdAt: timestamp,
206
+ ownerEmail: input.ownerEmail,
207
+ orgId: input.orgId,
208
+ });
209
+ await tx.insert(schema.contextItemVersions).values({
210
+ id: itemVersionId,
211
+ itemId,
212
+ versionNumber: Number(versionNumber ?? 1),
213
+ contentHash: input.item.contentHash,
214
+ title: input.item.title,
215
+ content: input.item.content,
216
+ summary: input.item.summary ?? null,
217
+ mimeType: input.item.mimeType ?? null,
218
+ sourceModifiedAt: input.item.sourceModifiedAt ?? null,
219
+ sourceVersion: input.item.sourceVersion ?? null,
220
+ rawSnapshotBlobRef: input.item.rawSnapshotBlobRef ?? null,
221
+ parseStatus: input.item.parseStatus ?? "parsed",
222
+ parseError: input.item.parseError ?? null,
223
+ metadata: stringifyJson(input.item.metadata),
224
+ createdAt: timestamp,
225
+ ownerEmail: input.ownerEmail,
226
+ orgId: input.orgId,
227
+ });
228
+ const chunks = input.item.chunks?.length
229
+ ? input.item.chunks
230
+ : [{ ordinal: 0, kind: "text", text: input.item.content }];
231
+ await tx.insert(schema.contextChunks).values(chunks.map((chunk) => ({
232
+ id: newId("ccc"),
233
+ itemId,
234
+ itemVersionId,
235
+ ordinal: chunk.ordinal,
236
+ kind: chunk.kind ?? "text",
237
+ text: chunk.text,
238
+ startOffset: chunk.startOffset ?? null,
239
+ endOffset: chunk.endOffset ?? null,
240
+ tokenCount: chunk.tokenCount ?? null,
241
+ metadata: stringifyJson(chunk.metadata),
242
+ createdAt: timestamp,
243
+ ownerEmail: input.ownerEmail,
244
+ orgId: input.orgId,
245
+ })));
246
+ if (input.item.media?.length)
247
+ await tx.insert(schema.contextMedia).values(input.item.media.map((media) => ({
248
+ id: newId("ccm"),
249
+ itemId,
250
+ itemVersionId,
251
+ kind: media.kind,
252
+ mimeType: media.mimeType ?? null,
253
+ accessMode: media.accessMode ?? "public",
254
+ url: media.accessMode === "public" ? (media.url ?? null) : null,
255
+ storageKey: media.storageKey ?? null,
256
+ provenanceUrl: media.provenanceUrl ?? media.url ?? null,
257
+ altText: media.altText ?? null,
258
+ caption: media.caption ?? null,
259
+ captionStatus: media.captionStatus ?? "pending",
260
+ ocrText: media.ocrText ?? null,
261
+ palette: stringifyJson(media.palette ?? []),
262
+ contentHash: media.contentHash ?? null,
263
+ width: media.width ?? null,
264
+ height: media.height ?? null,
265
+ durationMs: media.durationMs ?? null,
266
+ metadata: stringifyJson(media.metadata),
267
+ createdAt: timestamp,
268
+ ownerEmail: input.ownerEmail,
269
+ orgId: input.orgId,
270
+ })));
271
+ if (input.item.edges?.length)
272
+ await tx.insert(schema.contextEdges).values(input.item.edges.map((edge) => ({
273
+ id: newId("cce"),
274
+ fromItemId: itemId,
275
+ fromItemVersionId: itemVersionId,
276
+ toItemId: edge.toItemId ?? null,
277
+ toItemVersionId: edge.toItemVersionId ?? null,
278
+ toExternalId: edge.toExternalId ?? null,
279
+ relation: edge.relation,
280
+ metadata: stringifyJson(edge.metadata),
281
+ createdAt: timestamp,
282
+ ownerEmail: input.ownerEmail,
283
+ orgId: input.orgId,
284
+ })));
285
+ await tx
286
+ .update(schema.contextSources)
287
+ .set({ updatedAt: timestamp })
288
+ .where(eq(schema.contextSources.id, input.sourceId));
289
+ });
290
+ return { itemId, itemVersionId };
291
+ }
292
+ async function readSnapshot(itemId, itemVersionId) {
293
+ const { getDb, schema } = getCreativeContext();
294
+ const [item] = await getDb()
295
+ .select()
296
+ .from(schema.contextItems)
297
+ .where(eq(schema.contextItems.id, itemId))
298
+ .limit(1);
299
+ const [version] = await getDb()
300
+ .select()
301
+ .from(schema.contextItemVersions)
302
+ .where(eq(schema.contextItemVersions.id, itemVersionId))
303
+ .limit(1);
304
+ if (!item || !version)
305
+ throw new Error("Staged context snapshot was not found");
306
+ const [chunks, media, edges] = await Promise.all([
307
+ getDb()
308
+ .select()
309
+ .from(schema.contextChunks)
310
+ .where(eq(schema.contextChunks.itemVersionId, itemVersionId)),
311
+ getDb()
312
+ .select()
313
+ .from(schema.contextMedia)
314
+ .where(eq(schema.contextMedia.itemVersionId, itemVersionId)),
315
+ getDb()
316
+ .select()
317
+ .from(schema.contextEdges)
318
+ .where(eq(schema.contextEdges.fromItemVersionId, itemVersionId)),
319
+ ]);
320
+ return {
321
+ externalId: item.externalId,
322
+ kind: item.kind,
323
+ title: version.title,
324
+ canonicalUrl: item.canonicalUrl ?? undefined,
325
+ mimeType: version.mimeType ?? undefined,
326
+ content: version.content,
327
+ summary: version.summary ?? undefined,
328
+ contentHash: version.contentHash,
329
+ sourceModifiedAt: version.sourceModifiedAt ?? undefined,
330
+ sourceVersion: version.sourceVersion ?? undefined,
331
+ rawSnapshotBlobRef: version.rawSnapshotBlobRef ?? undefined,
332
+ parseStatus: version.parseStatus,
333
+ parseError: version.parseError ?? undefined,
334
+ upstreamAccess: item.upstreamAccess,
335
+ curationStatus: "included",
336
+ curationRank: item.curationRank,
337
+ tags: parseJson(item.tags, []),
338
+ colors: parseJson(item.colors, []),
339
+ provenance: parseJson(item.provenance, {}),
340
+ thumbnailBlobRef: item.thumbnailBlobRef ?? undefined,
341
+ metadata: parseJson(version.metadata, {}),
342
+ chunks: chunks.map((row) => ({
343
+ ordinal: row.ordinal,
344
+ kind: row.kind,
345
+ text: row.text,
346
+ startOffset: row.startOffset ?? undefined,
347
+ endOffset: row.endOffset ?? undefined,
348
+ tokenCount: row.tokenCount ?? undefined,
349
+ metadata: parseJson(row.metadata, {}),
350
+ })),
351
+ media: media.map((row) => ({
352
+ kind: row.kind,
353
+ mimeType: row.mimeType ?? undefined,
354
+ accessMode: row.accessMode,
355
+ url: row.url ?? undefined,
356
+ storageKey: row.storageKey ?? undefined,
357
+ provenanceUrl: row.provenanceUrl ?? undefined,
358
+ altText: row.altText ?? undefined,
359
+ caption: row.caption ?? undefined,
360
+ captionStatus: row.captionStatus,
361
+ ocrText: row.ocrText ?? undefined,
362
+ palette: parseJson(row.palette, []),
363
+ contentHash: row.contentHash ?? undefined,
364
+ width: row.width ?? undefined,
365
+ height: row.height ?? undefined,
366
+ durationMs: row.durationMs ?? undefined,
367
+ metadata: parseJson(row.metadata, {}),
368
+ })),
369
+ edges: edges.map((row) => ({
370
+ relation: row.relation,
371
+ ...(row.toItemId ? { toItemId: row.toItemId } : {}),
372
+ ...(row.toItemVersionId ? { toItemVersionId: row.toItemVersionId } : {}),
373
+ ...(row.toExternalId ? { toExternalId: row.toExternalId } : {}),
374
+ metadata: parseJson(row.metadata, {}),
375
+ })),
376
+ };
377
+ }
378
+ export async function createCreativeContext(input) {
379
+ const { getDb, schema } = getCreativeContext();
380
+ const actor = requireActor();
381
+ const timestamp = nowIso();
382
+ if (actor.orgId)
383
+ await assertCurrentRequestUserIsOrgAdmin(actor.orgId);
384
+ const defaultScopeKey = input.kind === "default" ? defaultContextScopeKey(actor) : null;
385
+ if (input.kind === "default") {
386
+ const existing = await getDb()
387
+ .select({ id: schema.creativeContexts.id })
388
+ .from(schema.creativeContexts)
389
+ .where(eq(schema.creativeContexts.defaultScopeKey, defaultScopeKey))
390
+ .limit(1);
391
+ if (existing[0])
392
+ return getCreativeContextById(existing[0].id);
393
+ }
394
+ const id = newId("ccx"), stagingSourceId = newId("ccs"), publishedSourceId = newId("ccs");
395
+ try {
396
+ await getDb().transaction(async (tx) => {
397
+ for (const [sourceId, name, purpose] of [
398
+ [stagingSourceId, `${input.name} staging`, "staging"],
399
+ [publishedSourceId, `${input.name} published`, "published"],
400
+ ])
401
+ await tx.insert(schema.contextSources).values({
402
+ id: sourceId,
403
+ name,
404
+ kind: "native-app",
405
+ config: stringifyJson({ governedContextId: id, purpose }),
406
+ upstreamAccess: "available",
407
+ status: "active",
408
+ healthStatus: "healthy",
409
+ itemCount: 0,
410
+ restrictedItemCount: 0,
411
+ createdAt: timestamp,
412
+ updatedAt: timestamp,
413
+ ownerEmail: actor.ownerEmail,
414
+ orgId: actor.orgId,
415
+ visibility: "private",
416
+ });
417
+ await tx.insert(schema.creativeContexts).values({
418
+ id,
419
+ name: input.name,
420
+ description: input.description ?? null,
421
+ kind: input.kind,
422
+ defaultScopeKey,
423
+ brandProfileId: input.brandProfileId ?? null,
424
+ stagingSourceId,
425
+ publishedSourceId,
426
+ approvalPolicy: input.approvalPolicy ?? "open",
427
+ archivedAt: null,
428
+ createdAt: timestamp,
429
+ updatedAt: timestamp,
430
+ ownerEmail: actor.ownerEmail,
431
+ orgId: actor.orgId,
432
+ visibility: actor.orgId ? "org" : "private",
433
+ });
434
+ await appendAudit(tx, id, "create", {
435
+ kind: input.kind,
436
+ approvalPolicy: input.approvalPolicy ?? "open",
437
+ });
438
+ });
439
+ }
440
+ catch (error) {
441
+ if (input.kind === "default") {
442
+ const existing = await getDb()
443
+ .select({ id: schema.creativeContexts.id })
444
+ .from(schema.creativeContexts)
445
+ .where(eq(schema.creativeContexts.defaultScopeKey, defaultScopeKey))
446
+ .limit(1);
447
+ if (existing[0])
448
+ return getCreativeContextById(existing[0].id);
449
+ }
450
+ throw error;
451
+ }
452
+ return getCreativeContextById(id);
453
+ }
454
+ /** Idempotently establishes the actor's governed Default with the currently usable corpus. */
455
+ export async function ensureDefaultCreativeContext() {
456
+ const { getDb, schema } = getCreativeContext();
457
+ const actor = requireActor();
458
+ const existing = await getDb()
459
+ .select()
460
+ .from(schema.creativeContexts)
461
+ .where(eq(schema.creativeContexts.defaultScopeKey, defaultContextScopeKey(actor)))
462
+ .limit(1);
463
+ if (existing[0])
464
+ return getCreativeContextById(existing[0].id);
465
+ if (actor.orgId && !(await currentRequestUserIsOrgAdmin(actor.orgId)))
466
+ return null;
467
+ const profileRows = await getDb()
468
+ .select({ id: schema.brandProfiles.id })
469
+ .from(schema.brandProfiles)
470
+ .innerJoin(schema.brandDnaVersions, eq(schema.brandDnaVersions.profileId, schema.brandProfiles.id))
471
+ .where(and(accessFilter(schema.brandProfiles, schema.brandProfileShares), eq(schema.brandDnaVersions.status, "published")))
472
+ .limit(1);
473
+ const created = await createCreativeContext({
474
+ name: "Default",
475
+ kind: "default",
476
+ brandProfileId: profileRows[0]?.id ?? null,
477
+ approvalPolicy: "open",
478
+ });
479
+ if (!created)
480
+ return null;
481
+ const corpus = await getDb()
482
+ .select({
483
+ itemId: schema.contextItems.id,
484
+ itemVersionId: schema.contextItems.currentVersionId,
485
+ sourceId: schema.contextSources.id,
486
+ externalId: schema.contextItems.externalId,
487
+ rank: schema.contextItems.curationRank,
488
+ })
489
+ .from(schema.contextItems)
490
+ .innerJoin(schema.contextSources, eq(schema.contextSources.id, schema.contextItems.sourceId))
491
+ .where(and(accessFilter(schema.contextSources, schema.contextSourceShares), ...(created.visibility === "org"
492
+ ? [inArray(schema.contextSources.visibility, ["org", "public"])]
493
+ : []), eq(schema.contextItems.curationStatus, "included"), eq(schema.contextItems.status, "active"), eq(schema.contextSources.status, "active"), eq(schema.contextSources.upstreamAccess, "available")))
494
+ .limit(5_000);
495
+ const timestamp = nowIso();
496
+ if (corpus.length)
497
+ await getDb().transaction(async (tx) => {
498
+ await tx.insert(schema.creativeContextMemberships).values(corpus.map((item) => ({
499
+ id: newId("ccmbr"),
500
+ contextId: created.id,
501
+ artifactKey: `${item.sourceId}:${item.externalId}`,
502
+ publishedItemId: item.itemId,
503
+ publishedItemVersionId: item.itemVersionId,
504
+ pendingSubmissionId: null,
505
+ rank: item.rank === "canonical" || item.rank === "exemplar"
506
+ ? item.rank
507
+ : "normal",
508
+ purpose: "Backfilled accessible corpus",
509
+ status: "active",
510
+ createdAt: timestamp,
511
+ updatedAt: timestamp,
512
+ ownerEmail: actor.ownerEmail,
513
+ orgId: actor.orgId,
514
+ })));
515
+ await appendAudit(tx, created.id, "backfill-default", {
516
+ members: corpus.length,
517
+ brandProfileId: profileRows[0]?.id ?? null,
518
+ });
519
+ });
520
+ return getCreativeContextById(created.id);
521
+ }
522
+ export async function getCreativeContextById(contextId) {
523
+ const access = await resolveAccess("creative-context", contextId);
524
+ if (!access)
525
+ return null;
526
+ const { getDb, schema } = getCreativeContext();
527
+ const [membershipCount] = await getDb()
528
+ .select({ value: count() })
529
+ .from(schema.creativeContextMemberships)
530
+ .where(and(eq(schema.creativeContextMemberships.contextId, contextId), eq(schema.creativeContextMemberships.status, "active")));
531
+ return mapContext(access.resource, Number(membershipCount?.value ?? 0), access.role);
532
+ }
533
+ export async function getCreativeContextAppBinding(appId) {
534
+ const { getDb, schema } = getCreativeContext();
535
+ const actor = requireActor();
536
+ const scope = actor.orgId
537
+ ? eq(schema.creativeContextAppBindings.orgId, actor.orgId)
538
+ : isNull(schema.creativeContextAppBindings.orgId);
539
+ const rows = await getDb()
540
+ .select({ contextId: schema.creativeContextAppBindings.contextId })
541
+ .from(schema.creativeContextAppBindings)
542
+ .where(and(eq(schema.creativeContextAppBindings.appId, appId), scope, ...(actor.orgId
543
+ ? []
544
+ : [
545
+ eq(schema.creativeContextAppBindings.ownerEmail, actor.ownerEmail),
546
+ ])))
547
+ .orderBy(asc(schema.creativeContextAppBindings.updatedAt))
548
+ .limit(1);
549
+ return rows[0] ? getCreativeContextById(rows[0].contextId) : null;
550
+ }
551
+ export async function listCreativeContexts(input) {
552
+ await ensureDefaultCreativeContext();
553
+ const { getDb, schema } = getCreativeContext();
554
+ const filters = [
555
+ accessFilter(schema.creativeContexts, schema.creativeContextShares),
556
+ ];
557
+ if (input.cursor)
558
+ filters.push(gt(schema.creativeContexts.id, input.cursor));
559
+ if (!input.includeArchived)
560
+ filters.push(isNull(schema.creativeContexts.archivedAt));
561
+ const rows = await getDb()
562
+ .select()
563
+ .from(schema.creativeContexts)
564
+ .where(and(...filters))
565
+ .orderBy(asc(schema.creativeContexts.id))
566
+ .limit(input.limit + 1);
567
+ const page = rows.slice(0, input.limit);
568
+ const counts = page.length
569
+ ? await getDb()
570
+ .select({
571
+ contextId: schema.creativeContextMemberships.contextId,
572
+ id: schema.creativeContextMemberships.id,
573
+ })
574
+ .from(schema.creativeContextMemberships)
575
+ .where(and(inArray(schema.creativeContextMemberships.contextId, page.map((row) => row.id)), eq(schema.creativeContextMemberships.status, "active")))
576
+ : [];
577
+ const byContext = new Map();
578
+ for (const row of counts)
579
+ byContext.set(row.contextId, (byContext.get(row.contextId) ?? 0) + 1);
580
+ const accessById = new Map((await Promise.all(page.map(async (row) => [row.id, await resolveAccess("creative-context", row.id)]))).flatMap(([id, access]) => (access ? [[id, access]] : [])));
581
+ return {
582
+ contexts: page.flatMap((row) => {
583
+ const access = accessById.get(row.id);
584
+ return access
585
+ ? [mapContext(row, byContext.get(row.id) ?? 0, access.role)]
586
+ : [];
587
+ }),
588
+ nextCursor: rows.length > input.limit ? page.at(-1)?.id : undefined,
589
+ };
590
+ }
591
+ export async function updateCreativeContext(contextId, patch) {
592
+ await assertContextRole(contextId, "admin");
593
+ const { getDb, schema } = getCreativeContext();
594
+ const values = { ...patch, updatedAt: nowIso() };
595
+ await getDb().transaction(async (tx) => {
596
+ await tx
597
+ .update(schema.creativeContexts)
598
+ .set(values)
599
+ .where(eq(schema.creativeContexts.id, contextId));
600
+ await appendAudit(tx, contextId, "update", { fields: Object.keys(patch) });
601
+ });
602
+ return getCreativeContextById(contextId);
603
+ }
604
+ export async function archiveCreativeContext(contextId) {
605
+ await assertContextRole(contextId, "admin");
606
+ const { getDb, schema } = getCreativeContext();
607
+ const [context] = await getDb()
608
+ .select({ kind: schema.creativeContexts.kind })
609
+ .from(schema.creativeContexts)
610
+ .where(eq(schema.creativeContexts.id, contextId))
611
+ .limit(1);
612
+ if (context?.kind === "default")
613
+ throw new Error("The Default Creative Context cannot be archived");
614
+ await getDb()
615
+ .update(schema.creativeContexts)
616
+ .set({ archivedAt: nowIso(), updatedAt: nowIso() })
617
+ .where(eq(schema.creativeContexts.id, contextId));
618
+ return getCreativeContextById(contextId);
619
+ }
620
+ export async function setCreativeContextAppDefault(contextId, appId) {
621
+ const access = await assertContextRole(contextId, "admin");
622
+ if (access.resource.orgId)
623
+ await assertCurrentRequestUserIsOrgAdmin(access.resource.orgId);
624
+ const { getDb, schema } = getCreativeContext();
625
+ const actor = requireActor();
626
+ const timestamp = nowIso();
627
+ await getDb().transaction(async (tx) => {
628
+ await tx
629
+ .delete(schema.creativeContextAppBindings)
630
+ .where(and(eq(schema.creativeContextAppBindings.appId, appId), actor.orgId
631
+ ? eq(schema.creativeContextAppBindings.orgId, actor.orgId)
632
+ : and(isNull(schema.creativeContextAppBindings.orgId), eq(schema.creativeContextAppBindings.ownerEmail, actor.ownerEmail))));
633
+ await tx.insert(schema.creativeContextAppBindings).values({
634
+ id: newId("ccab"),
635
+ appId,
636
+ contextId,
637
+ createdAt: timestamp,
638
+ updatedAt: timestamp,
639
+ ownerEmail: actor.ownerEmail,
640
+ orgId: actor.orgId,
641
+ });
642
+ await appendAudit(tx, contextId, "set-app-default", { appId });
643
+ });
644
+ return getCreativeContextById(contextId);
645
+ }
646
+ export async function listContextMemberships(input) {
647
+ const access = await assertContextRole(input.contextId, "viewer");
648
+ const { getDb, schema } = getCreativeContext();
649
+ const actor = requireActor();
650
+ const canReview = access.role === "owner" || access.role === "admin";
651
+ const filters = [
652
+ eq(schema.creativeContextMemberships.contextId, input.contextId),
653
+ ];
654
+ if (input.status)
655
+ filters.push(eq(schema.creativeContextMemberships.status, input.status));
656
+ if (input.cursor)
657
+ filters.push(gt(schema.creativeContextMemberships.id, input.cursor));
658
+ if (!canReview)
659
+ filters.push(or(isNotNull(schema.creativeContextMemberships.publishedItemId), eq(schema.creativeContextSubmissions.submittedBy, actor.ownerEmail)));
660
+ const rows = await getDb()
661
+ .select({
662
+ membership: schema.creativeContextMemberships,
663
+ pendingSubmission: schema.creativeContextSubmissions,
664
+ })
665
+ .from(schema.creativeContextMemberships)
666
+ .leftJoin(schema.creativeContextSubmissions, eq(schema.creativeContextSubmissions.id, schema.creativeContextMemberships.pendingSubmissionId))
667
+ .where(and(...filters))
668
+ .orderBy(asc(schema.creativeContextMemberships.id))
669
+ .limit(input.limit + 1);
670
+ const page = rows.slice(0, input.limit);
671
+ const canViewPendingSubmission = (submission) => Boolean(submission && (canReview || submission.submittedBy === actor.ownerEmail));
672
+ const publishedVersionIds = page.flatMap(({ membership }) => membership.publishedItemVersionId
673
+ ? [membership.publishedItemVersionId]
674
+ : []);
675
+ const pendingVersionIds = page.flatMap(({ pendingSubmission }) => canViewPendingSubmission(pendingSubmission) &&
676
+ pendingSubmission.stagingItemVersionId
677
+ ? [pendingSubmission.stagingItemVersionId]
678
+ : []);
679
+ const previewVersionIds = [
680
+ ...new Set([...publishedVersionIds, ...pendingVersionIds]),
681
+ ];
682
+ const [previewItems, previewMedia] = previewVersionIds.length
683
+ ? await Promise.all([
684
+ getDb()
685
+ .select({
686
+ id: schema.contextItems.id,
687
+ itemVersionId: schema.contextItemVersions.id,
688
+ title: schema.contextItemVersions.title,
689
+ kind: schema.contextItems.kind,
690
+ canonicalUrl: schema.contextItems.canonicalUrl,
691
+ status: schema.contextItems.status,
692
+ sourceModifiedAt: schema.contextItemVersions.sourceModifiedAt,
693
+ metadata: schema.contextItemVersions.metadata,
694
+ })
695
+ .from(schema.contextItemVersions)
696
+ .innerJoin(schema.contextItems, eq(schema.contextItems.id, schema.contextItemVersions.itemId))
697
+ .where(inArray(schema.contextItemVersions.id, previewVersionIds)),
698
+ getDb()
699
+ .select({
700
+ id: schema.contextMedia.id,
701
+ itemVersionId: schema.contextMedia.itemVersionId,
702
+ kind: schema.contextMedia.kind,
703
+ mimeType: schema.contextMedia.mimeType,
704
+ })
705
+ .from(schema.contextMedia)
706
+ .where(inArray(schema.contextMedia.itemVersionId, previewVersionIds)),
707
+ ])
708
+ : [[], []];
709
+ const mediaByVersion = new Map();
710
+ for (const media of previewMedia) {
711
+ const list = mediaByVersion.get(media.itemVersionId) ?? [];
712
+ list.push({
713
+ id: media.id,
714
+ kind: media.kind,
715
+ mimeType: media.mimeType ?? null,
716
+ url: creativeContextMediaUrl({ mediaId: media.id }),
717
+ });
718
+ mediaByVersion.set(media.itemVersionId, list);
719
+ }
720
+ const previewByVersion = new Map(previewItems.map((item) => {
721
+ const preview = sanitizePublicMetadata(parseJson(item.metadata, {}).preview);
722
+ return [
723
+ item.itemVersionId,
724
+ {
725
+ id: item.id,
726
+ itemVersionId: item.itemVersionId,
727
+ title: item.title,
728
+ kind: item.kind,
729
+ canonicalUrl: typeof sanitizePublicMetadata(item.canonicalUrl) === "string"
730
+ ? sanitizePublicMetadata(item.canonicalUrl)
731
+ : null,
732
+ status: item.status,
733
+ sourceModifiedAt: item.sourceModifiedAt ?? null,
734
+ media: mediaByVersion.get(item.itemVersionId) ?? [],
735
+ ...(preview && typeof preview === "object" && !Array.isArray(preview)
736
+ ? { preview: preview }
737
+ : {}),
738
+ },
739
+ ];
740
+ }));
741
+ return {
742
+ memberships: page.map(({ membership, pendingSubmission }) => {
743
+ const canViewPending = canViewPendingSubmission(pendingSubmission);
744
+ return {
745
+ ...mapMembership(canViewPending
746
+ ? membership
747
+ : { ...membership, pendingSubmissionId: null }),
748
+ publishedItem: membership.publishedItemVersionId
749
+ ? (previewByVersion.get(membership.publishedItemVersionId) ?? null)
750
+ : null,
751
+ pendingSubmission: canViewPending && pendingSubmission
752
+ ? {
753
+ ...mapSubmission(pendingSubmission),
754
+ proposedItem: pendingSubmission.stagingItemVersionId
755
+ ? (previewByVersion.get(pendingSubmission.stagingItemVersionId) ?? null)
756
+ : null,
757
+ }
758
+ : null,
759
+ };
760
+ }),
761
+ nextCursor: rows.length > input.limit ? page.at(-1)?.membership.id : undefined,
762
+ };
763
+ }
764
+ /**
765
+ * Resolves private media for a pending submission without making the staged
766
+ * item generally readable. This is intentionally server-only: callers must
767
+ * already have an authenticated request context and can only read the exact
768
+ * staged version they submitted or are allowed to review.
769
+ */
770
+ export async function readPendingCreativeContextMedia(input) {
771
+ const { getDb, schema } = getCreativeContext();
772
+ const actor = requireActor();
773
+ let itemId = input.itemId;
774
+ let itemVersionId = input.itemVersionId;
775
+ let media = null;
776
+ if (input.mediaId) {
777
+ const rows = await getDb()
778
+ .select({
779
+ id: schema.contextMedia.id,
780
+ itemId: schema.contextMedia.itemId,
781
+ itemVersionId: schema.contextMedia.itemVersionId,
782
+ storageKey: schema.contextMedia.storageKey,
783
+ mimeType: schema.contextMedia.mimeType,
784
+ })
785
+ .from(schema.contextMedia)
786
+ .where(eq(schema.contextMedia.id, input.mediaId))
787
+ .limit(1);
788
+ media = rows[0] ?? null;
789
+ if (!media)
790
+ return null;
791
+ if ((itemId && itemId !== media.itemId) ||
792
+ (itemVersionId && itemVersionId !== media.itemVersionId)) {
793
+ return null;
794
+ }
795
+ itemId = media.itemId;
796
+ itemVersionId = media.itemVersionId;
797
+ }
798
+ if (!itemId || !itemVersionId)
799
+ return null;
800
+ const submissions = await getDb()
801
+ .select({
802
+ contextId: schema.creativeContextSubmissions.contextId,
803
+ submittedBy: schema.creativeContextSubmissions.submittedBy,
804
+ })
805
+ .from(schema.creativeContextSubmissions)
806
+ .where(and(eq(schema.creativeContextSubmissions.status, "pending"), eq(schema.creativeContextSubmissions.stagingItemId, itemId), eq(schema.creativeContextSubmissions.stagingItemVersionId, itemVersionId)))
807
+ .limit(1);
808
+ const submission = submissions[0];
809
+ if (!submission)
810
+ return null;
811
+ const access = await resolveAccess("creative-context", submission.contextId);
812
+ if (!access)
813
+ return null;
814
+ const canReview = access.role === "owner" || access.role === "admin";
815
+ if (!canReview && submission.submittedBy !== actor.ownerEmail)
816
+ return null;
817
+ if (media) {
818
+ return {
819
+ itemId,
820
+ itemVersionId,
821
+ mediaId: media.id,
822
+ storageKey: media.storageKey,
823
+ mimeType: media.mimeType,
824
+ };
825
+ }
826
+ const rows = await getDb()
827
+ .select({
828
+ thumbnailBlobRef: schema.contextItems.thumbnailBlobRef,
829
+ mimeType: schema.contextItemVersions.mimeType,
830
+ })
831
+ .from(schema.contextItemVersions)
832
+ .innerJoin(schema.contextItems, eq(schema.contextItems.id, schema.contextItemVersions.itemId))
833
+ .where(and(eq(schema.contextItems.id, itemId), eq(schema.contextItemVersions.id, itemVersionId)))
834
+ .limit(1);
835
+ const item = rows[0];
836
+ return item
837
+ ? {
838
+ itemId,
839
+ itemVersionId,
840
+ mediaId: null,
841
+ storageKey: item.thumbnailBlobRef,
842
+ mimeType: item.mimeType,
843
+ }
844
+ : null;
845
+ }
846
+ async function resolveSubmissionItem(input) {
847
+ if (input.nativeResource) {
848
+ const captured = await captureNativeCreativeResource(input.nativeResource);
849
+ if (!captured.items.length)
850
+ throw new Error("Native capture returned no artifacts");
851
+ return {
852
+ artifactKey: captured.artifactKey,
853
+ items: captured.items,
854
+ privateMetadata: {
855
+ ...(captured.privateMetadata ?? {}),
856
+ nativeResource: input.nativeResource,
857
+ },
858
+ sourceAccess: captured.source.access ?? null,
859
+ };
860
+ }
861
+ if (!input.itemId)
862
+ throw new Error("itemId or nativeResource is required");
863
+ const detail = await getCreativeContextItem(input.itemId, input.itemVersionId);
864
+ if (!detail)
865
+ throw new Error("Context item version not found or not accessible");
866
+ const sourceAccess = await resolveAccess("creative-context-source", detail.item.sourceId);
867
+ const children = await Promise.all(detail.edges
868
+ .filter((edge) => edge.relation === "contains-native-child" &&
869
+ typeof edge.toItemId === "string")
870
+ .map((edge) => getCreativeContextItem(edge.toItemId, edge.toItemVersionId ?? undefined)));
871
+ return {
872
+ artifactKey: `${detail.item.sourceId}:${detail.item.externalId}`,
873
+ items: [
874
+ normalizedFromDetail(detail),
875
+ ...children.flatMap((child) => child ? [normalizedFromDetail(child)] : []),
876
+ ],
877
+ privateMetadata: {},
878
+ sourceAccess: sourceAccess
879
+ ? {
880
+ visibility: sourceAccess.resource.visibility,
881
+ canManage: sourceAccess.role === "owner" || sourceAccess.role === "admin",
882
+ }
883
+ : null,
884
+ };
885
+ }
886
+ function visibilityRank(visibility) {
887
+ return visibility === "public" ? 2 : visibility === "org" ? 1 : 0;
888
+ }
889
+ async function approveSubmission(context, membership, submission) {
890
+ const { getDb, schema } = getCreativeContext();
891
+ const actor = requireActor();
892
+ const staged = await readSnapshot(submission.stagingItemId, submission.stagingItemVersionId);
893
+ const published = await writeSnapshot({
894
+ sourceId: context.publishedSourceId,
895
+ artifactKey: submission.artifactKey,
896
+ item: staged,
897
+ ownerEmail: context.ownerEmail,
898
+ orgId: context.orgId ?? null,
899
+ });
900
+ const privateMetadata = parseJson(submission.privateMetadata, {});
901
+ const publishedChildren = [];
902
+ for (const child of Array.isArray(privateMetadata.stagedChildren)
903
+ ? privateMetadata.stagedChildren
904
+ : []) {
905
+ if (typeof child.artifactKey !== "string" ||
906
+ typeof child.itemId !== "string" ||
907
+ typeof child.itemVersionId !== "string")
908
+ continue;
909
+ const publishedChild = await writeSnapshot({
910
+ sourceId: context.publishedSourceId,
911
+ artifactKey: child.artifactKey,
912
+ item: await readSnapshot(child.itemId, child.itemVersionId),
913
+ ownerEmail: context.ownerEmail,
914
+ orgId: context.orgId ?? null,
915
+ });
916
+ publishedChildren.push({
917
+ artifactKey: child.artifactKey,
918
+ ...publishedChild,
919
+ });
920
+ }
921
+ for (const child of publishedChildren) {
922
+ await getDb()
923
+ .update(schema.contextEdges)
924
+ .set({ toItemId: child.itemId, toItemVersionId: child.itemVersionId })
925
+ .where(and(eq(schema.contextEdges.fromItemVersionId, published.itemVersionId), eq(schema.contextEdges.toExternalId, child.artifactKey)));
926
+ }
927
+ const timestamp = nowIso();
928
+ await getDb().transaction(async (tx) => {
929
+ await tx
930
+ .update(schema.creativeContextSubmissions)
931
+ .set({
932
+ status: "approved",
933
+ publishedItemId: published.itemId,
934
+ publishedItemVersionId: published.itemVersionId,
935
+ reviewedBy: actor.ownerEmail,
936
+ reviewedAt: timestamp,
937
+ })
938
+ .where(eq(schema.creativeContextSubmissions.id, submission.id));
939
+ await tx
940
+ .update(schema.creativeContextMemberships)
941
+ .set({
942
+ publishedItemId: published.itemId,
943
+ publishedItemVersionId: published.itemVersionId,
944
+ pendingSubmissionId: null,
945
+ status: "active",
946
+ updatedAt: timestamp,
947
+ })
948
+ .where(eq(schema.creativeContextMemberships.id, membership.id));
949
+ for (const child of publishedChildren) {
950
+ const existingChild = await tx
951
+ .select({ id: schema.creativeContextMemberships.id })
952
+ .from(schema.creativeContextMemberships)
953
+ .where(and(eq(schema.creativeContextMemberships.contextId, context.id), eq(schema.creativeContextMemberships.artifactKey, child.artifactKey)))
954
+ .limit(1);
955
+ const values = {
956
+ publishedItemId: child.itemId,
957
+ publishedItemVersionId: child.itemVersionId,
958
+ pendingSubmissionId: null,
959
+ status: "active",
960
+ updatedAt: timestamp,
961
+ };
962
+ if (existingChild[0]) {
963
+ await tx
964
+ .update(schema.creativeContextMemberships)
965
+ .set(values)
966
+ .where(eq(schema.creativeContextMemberships.id, existingChild[0].id));
967
+ }
968
+ else {
969
+ await tx.insert(schema.creativeContextMemberships).values({
970
+ id: newId("ccmbr"),
971
+ contextId: context.id,
972
+ artifactKey: child.artifactKey,
973
+ ...values,
974
+ rank: "normal",
975
+ purpose: "Native artifact child",
976
+ createdAt: timestamp,
977
+ ownerEmail: context.ownerEmail,
978
+ orgId: context.orgId ?? null,
979
+ });
980
+ }
981
+ }
982
+ await tx.insert(schema.creativeContextPublishedSnapshots).values([
983
+ {
984
+ id: newId("ccps"),
985
+ contextId: context.id,
986
+ sourceId: context.publishedSourceId,
987
+ membershipId: membership.id,
988
+ itemId: published.itemId,
989
+ itemVersionId: published.itemVersionId,
990
+ submissionId: submission.id,
991
+ createdAt: timestamp,
992
+ ownerEmail: context.ownerEmail,
993
+ orgId: context.orgId ?? null,
994
+ },
995
+ ...publishedChildren.map((child) => ({
996
+ id: newId("ccps"),
997
+ contextId: context.id,
998
+ sourceId: context.publishedSourceId,
999
+ membershipId: membership.id,
1000
+ itemId: child.itemId,
1001
+ itemVersionId: child.itemVersionId,
1002
+ submissionId: submission.id,
1003
+ createdAt: timestamp,
1004
+ ownerEmail: context.ownerEmail,
1005
+ orgId: context.orgId ?? null,
1006
+ })),
1007
+ ]);
1008
+ await appendAudit(tx, context.id, "approve-submission", {
1009
+ submissionId: submission.id,
1010
+ membershipId: membership.id,
1011
+ });
1012
+ });
1013
+ }
1014
+ export async function submitLatestContextMembershipUpdate(input) {
1015
+ await assertContextRole(input.contextId, "editor");
1016
+ const configured = getCreativeContext();
1017
+ const { getDb, schema } = configured;
1018
+ const [published] = await getDb()
1019
+ .select({
1020
+ membership: schema.creativeContextMemberships,
1021
+ sourceModifiedAt: schema.contextItemVersions.sourceModifiedAt,
1022
+ })
1023
+ .from(schema.creativeContextMemberships)
1024
+ .innerJoin(schema.contextItemVersions, eq(schema.contextItemVersions.id, schema.creativeContextMemberships.publishedItemVersionId))
1025
+ .where(and(eq(schema.creativeContextMemberships.id, input.membershipId), eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.status, "active")))
1026
+ .limit(1);
1027
+ if (!published)
1028
+ throw new Error("Published context membership not found");
1029
+ const reference = parseNativeCreativeArtifactKey(published.membership.artifactKey);
1030
+ if (!reference || reference.appId !== configured.connectorContext.appId) {
1031
+ throw new Error("This context membership is not backed by a native resource in the active app");
1032
+ }
1033
+ const statuses = await resolveNativeCreativeResourceUpdateStatuses([
1034
+ {
1035
+ key: published.membership.id,
1036
+ ...reference,
1037
+ publishedSourceModifiedAt: published.sourceModifiedAt,
1038
+ },
1039
+ ]);
1040
+ const status = statuses.get(published.membership.id);
1041
+ if (!status)
1042
+ throw new Error("Native resource not found or update status is unavailable");
1043
+ if (status.state !== "update-available")
1044
+ throw new Error("This context membership is already up to date");
1045
+ return manageContextMembership({
1046
+ operation: "submit",
1047
+ contextId: input.contextId,
1048
+ nativeResource: status.reference,
1049
+ note: input.note,
1050
+ rank: published.membership.rank,
1051
+ purpose: published.membership.purpose ?? undefined,
1052
+ confirmBroaderPublication: input.confirmBroaderPublication,
1053
+ });
1054
+ }
1055
+ export async function manageContextMembership(input) {
1056
+ const { getDb, schema } = getCreativeContext();
1057
+ const access = await assertContextRole(input.contextId, input.operation === "submit" ? "editor" : "viewer");
1058
+ const [context] = await getDb()
1059
+ .select()
1060
+ .from(schema.creativeContexts)
1061
+ .where(eq(schema.creativeContexts.id, access.resource.id))
1062
+ .limit(1);
1063
+ if (!context)
1064
+ throw new Error("Creative Context no longer exists");
1065
+ const actor = requireActor();
1066
+ if (input.operation === "submit") {
1067
+ const captured = await resolveSubmissionItem(input);
1068
+ if (!captured.sourceAccess ||
1069
+ visibilityRank(context.visibility) >
1070
+ visibilityRank(captured.sourceAccess.visibility)) {
1071
+ if (!captured.sourceAccess?.canManage) {
1072
+ throw new Error("Only a source manager may publish this artifact into a broader Creative Context");
1073
+ }
1074
+ if (!input.confirmBroaderPublication) {
1075
+ throw new Error("Confirm broader publication before submitting an artifact into a broader Creative Context");
1076
+ }
1077
+ }
1078
+ const root = captured.items[0];
1079
+ if (!root)
1080
+ throw new Error("Submission did not include a root artifact");
1081
+ const timestamp = nowIso();
1082
+ let membership = (await getDb()
1083
+ .select()
1084
+ .from(schema.creativeContextMemberships)
1085
+ .where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.artifactKey, captured.artifactKey)))
1086
+ .limit(1))[0];
1087
+ const membershipId = membership?.id ?? newId("ccmbr");
1088
+ const staged = await writeSnapshot({
1089
+ sourceId: context.stagingSourceId,
1090
+ artifactKey: captured.artifactKey,
1091
+ item: root,
1092
+ ownerEmail: context.ownerEmail,
1093
+ orgId: context.orgId ?? null,
1094
+ });
1095
+ const stagedChildren = await Promise.all(captured.items.slice(1).map(async (item) => {
1096
+ const snapshot = await writeSnapshot({
1097
+ sourceId: context.stagingSourceId,
1098
+ artifactKey: item.externalId,
1099
+ item,
1100
+ ownerEmail: context.ownerEmail,
1101
+ orgId: context.orgId ?? null,
1102
+ });
1103
+ return {
1104
+ artifactKey: item.externalId,
1105
+ ...snapshot,
1106
+ };
1107
+ }));
1108
+ for (const child of stagedChildren) {
1109
+ await getDb()
1110
+ .update(schema.contextEdges)
1111
+ .set({ toItemId: child.itemId, toItemVersionId: child.itemVersionId })
1112
+ .where(and(eq(schema.contextEdges.fromItemVersionId, staged.itemVersionId), eq(schema.contextEdges.toExternalId, child.artifactKey)));
1113
+ }
1114
+ const submissionId = newId("ccsub");
1115
+ const autoApprove = context.approvalPolicy === "open" ||
1116
+ (context.approvalPolicy === "admins-only" &&
1117
+ (await currentRequestUserIsOrgAdmin(context.orgId ?? undefined)));
1118
+ await getDb().transaction(async (tx) => {
1119
+ if (membership?.pendingSubmissionId)
1120
+ await tx
1121
+ .update(schema.creativeContextSubmissions)
1122
+ .set({
1123
+ status: "superseded",
1124
+ reviewedBy: actor.ownerEmail,
1125
+ reviewedAt: timestamp,
1126
+ })
1127
+ .where(eq(schema.creativeContextSubmissions.id, membership.pendingSubmissionId));
1128
+ if (!membership)
1129
+ await tx.insert(schema.creativeContextMemberships).values({
1130
+ id: membershipId,
1131
+ contextId: input.contextId,
1132
+ artifactKey: captured.artifactKey,
1133
+ publishedItemId: null,
1134
+ publishedItemVersionId: null,
1135
+ pendingSubmissionId: submissionId,
1136
+ rank: input.rank ?? "normal",
1137
+ purpose: input.purpose ?? null,
1138
+ status: "active",
1139
+ createdAt: timestamp,
1140
+ updatedAt: timestamp,
1141
+ ownerEmail: context.ownerEmail,
1142
+ orgId: context.orgId ?? null,
1143
+ });
1144
+ else
1145
+ await tx
1146
+ .update(schema.creativeContextMemberships)
1147
+ .set({
1148
+ pendingSubmissionId: submissionId,
1149
+ rank: input.rank ?? membership.rank,
1150
+ purpose: input.purpose ?? membership.purpose,
1151
+ status: "active",
1152
+ updatedAt: timestamp,
1153
+ })
1154
+ .where(eq(schema.creativeContextMemberships.id, membershipId));
1155
+ await tx.insert(schema.creativeContextSubmissions).values({
1156
+ id: submissionId,
1157
+ contextId: input.contextId,
1158
+ membershipId,
1159
+ artifactKey: captured.artifactKey,
1160
+ stagingItemId: staged.itemId,
1161
+ stagingItemVersionId: staged.itemVersionId,
1162
+ publishedItemId: null,
1163
+ publishedItemVersionId: null,
1164
+ note: input.note ?? null,
1165
+ privateMetadata: stringifyJson({
1166
+ ...captured.privateMetadata,
1167
+ stagedChildren,
1168
+ }),
1169
+ status: "pending",
1170
+ submittedBy: actor.ownerEmail,
1171
+ reviewedBy: null,
1172
+ reviewNote: null,
1173
+ createdAt: timestamp,
1174
+ reviewedAt: null,
1175
+ ownerEmail: context.ownerEmail,
1176
+ orgId: context.orgId ?? null,
1177
+ });
1178
+ await appendAudit(tx, input.contextId, "submit", {
1179
+ membershipId,
1180
+ submissionId,
1181
+ childCount: stagedChildren.length,
1182
+ });
1183
+ });
1184
+ membership = { id: membershipId };
1185
+ const submission = (await getDb()
1186
+ .select()
1187
+ .from(schema.creativeContextSubmissions)
1188
+ .where(eq(schema.creativeContextSubmissions.id, submissionId))
1189
+ .limit(1))[0];
1190
+ if (autoApprove)
1191
+ await approveSubmission(context, membership, submission);
1192
+ return {
1193
+ membershipId,
1194
+ submission: mapSubmission((await getDb()
1195
+ .select()
1196
+ .from(schema.creativeContextSubmissions)
1197
+ .where(eq(schema.creativeContextSubmissions.id, submissionId))
1198
+ .limit(1))[0]),
1199
+ };
1200
+ }
1201
+ if (!input.membershipId)
1202
+ throw new Error("membershipId is required");
1203
+ const membership = (await getDb()
1204
+ .select()
1205
+ .from(schema.creativeContextMemberships)
1206
+ .where(and(eq(schema.creativeContextMemberships.id, input.membershipId), eq(schema.creativeContextMemberships.contextId, input.contextId)))
1207
+ .limit(1))[0];
1208
+ if (!membership)
1209
+ throw new Error("Context membership not found");
1210
+ if (input.operation === "remove") {
1211
+ await requireReviewer(input.contextId);
1212
+ const timestamp = nowIso();
1213
+ await getDb().transaction(async (tx) => {
1214
+ await tx
1215
+ .update(schema.creativeContextMemberships)
1216
+ .set({
1217
+ status: "removed",
1218
+ pendingSubmissionId: null,
1219
+ updatedAt: timestamp,
1220
+ })
1221
+ .where(eq(schema.creativeContextMemberships.id, membership.id));
1222
+ const snapshots = await tx
1223
+ .select({
1224
+ itemId: schema.creativeContextPublishedSnapshots.itemId,
1225
+ itemVersionId: schema.creativeContextPublishedSnapshots.itemVersionId,
1226
+ })
1227
+ .from(schema.creativeContextPublishedSnapshots)
1228
+ .where(eq(schema.creativeContextPublishedSnapshots.membershipId, membership.id));
1229
+ const dependencies = snapshots.filter((snapshot) => snapshot.itemId !== membership.publishedItemId ||
1230
+ snapshot.itemVersionId !== membership.publishedItemVersionId);
1231
+ if (!dependencies.length)
1232
+ return;
1233
+ const dependentMemberships = await tx
1234
+ .select({ id: schema.creativeContextMemberships.id })
1235
+ .from(schema.creativeContextMemberships)
1236
+ .where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.status, "active"), eq(schema.creativeContextMemberships.purpose, "Native artifact child"), or(...dependencies.map((dependency) => and(eq(schema.creativeContextMemberships.publishedItemId, dependency.itemId), eq(schema.creativeContextMemberships.publishedItemVersionId, dependency.itemVersionId))))));
1237
+ const dependentIds = dependentMemberships.map((row) => row.id);
1238
+ if (dependentIds.length) {
1239
+ await tx
1240
+ .update(schema.creativeContextMemberships)
1241
+ .set({
1242
+ status: "removed",
1243
+ pendingSubmissionId: null,
1244
+ updatedAt: timestamp,
1245
+ })
1246
+ .where(inArray(schema.creativeContextMemberships.id, dependentIds));
1247
+ }
1248
+ });
1249
+ return {
1250
+ membership: mapMembership({
1251
+ ...membership,
1252
+ status: "removed",
1253
+ pendingSubmissionId: null,
1254
+ }),
1255
+ };
1256
+ }
1257
+ const submission = membership.pendingSubmissionId
1258
+ ? (await getDb()
1259
+ .select()
1260
+ .from(schema.creativeContextSubmissions)
1261
+ .where(eq(schema.creativeContextSubmissions.id, membership.pendingSubmissionId))
1262
+ .limit(1))[0]
1263
+ : null;
1264
+ if (!submission)
1265
+ throw new Error("No pending submission for this membership");
1266
+ if (input.operation === "withdraw") {
1267
+ if (submission.submittedBy !== actor.ownerEmail)
1268
+ await requireReviewer(input.contextId);
1269
+ await getDb().transaction(async (tx) => {
1270
+ await tx
1271
+ .update(schema.creativeContextSubmissions)
1272
+ .set({
1273
+ status: "withdrawn",
1274
+ reviewedBy: actor.ownerEmail,
1275
+ reviewedAt: nowIso(),
1276
+ })
1277
+ .where(eq(schema.creativeContextSubmissions.id, submission.id));
1278
+ await tx
1279
+ .update(schema.creativeContextMemberships)
1280
+ .set({ pendingSubmissionId: null, updatedAt: nowIso() })
1281
+ .where(eq(schema.creativeContextMemberships.id, membership.id));
1282
+ });
1283
+ return { withdrawn: true };
1284
+ }
1285
+ await requireReviewer(input.contextId);
1286
+ if (input.operation === "request-changes") {
1287
+ await getDb().transaction(async (tx) => {
1288
+ await tx
1289
+ .update(schema.creativeContextSubmissions)
1290
+ .set({
1291
+ status: "rejected",
1292
+ reviewedBy: actor.ownerEmail,
1293
+ reviewNote: input.note ?? "Changes requested",
1294
+ reviewedAt: nowIso(),
1295
+ })
1296
+ .where(eq(schema.creativeContextSubmissions.id, submission.id));
1297
+ await tx
1298
+ .update(schema.creativeContextMemberships)
1299
+ .set({ pendingSubmissionId: null, updatedAt: nowIso() })
1300
+ .where(eq(schema.creativeContextMemberships.id, membership.id));
1301
+ await appendAudit(tx, input.contextId, "request-changes", {
1302
+ submissionId: submission.id,
1303
+ });
1304
+ });
1305
+ return { requestChanges: true };
1306
+ }
1307
+ await approveSubmission(context, membership, submission);
1308
+ return { approved: true };
1309
+ }
1310
+ export async function resolveNativeContextCloneReference(input) {
1311
+ await assertContextRole(input.contextId, "viewer");
1312
+ const { getDb, schema } = getCreativeContext();
1313
+ const [membership] = await getDb()
1314
+ .select()
1315
+ .from(schema.creativeContextMemberships)
1316
+ .where(and(eq(schema.creativeContextMemberships.contextId, input.contextId), eq(schema.creativeContextMemberships.artifactKey, input.artifactKey), eq(schema.creativeContextMemberships.status, "active")))
1317
+ .limit(1);
1318
+ if (!membership?.publishedItemId || !membership?.publishedItemVersionId)
1319
+ throw new Error("Creative context artifact is not published");
1320
+ const [submission] = await getDb()
1321
+ .select()
1322
+ .from(schema.creativeContextSubmissions)
1323
+ .where(and(eq(schema.creativeContextSubmissions.membershipId, membership.id), eq(schema.creativeContextSubmissions.status, "approved"), eq(schema.creativeContextSubmissions.publishedItemVersionId, membership.publishedItemVersionId)))
1324
+ .limit(1);
1325
+ const metadata = parseJson(submission?.privateMetadata, {});
1326
+ const clone = metadata.clone;
1327
+ if (!clone ||
1328
+ !clone.handle ||
1329
+ clone.appId !== input.appId ||
1330
+ clone.resourceType !== input.resourceType ||
1331
+ clone.resourceId !== input.resourceId ||
1332
+ (input.expectedUpdatedAt !== undefined &&
1333
+ clone.updatedAt !== input.expectedUpdatedAt))
1334
+ throw new Error("Native creative resource reference does not match the governed context submission");
1335
+ return {
1336
+ publishedItemId: membership.publishedItemId,
1337
+ publishedItemVersionId: membership.publishedItemVersionId,
1338
+ cloneHandle: clone.handle,
1339
+ };
1340
+ }
1341
+ //# sourceMappingURL=contexts.js.map