@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,121 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ parseNativeCreativeArtifactKey,
5
+ registerNativeResourceCaptureAdapter,
6
+ resolveNativeCreativeResourceUpdateStatuses,
7
+ unregisterNativeResourceCaptureAdapter,
8
+ } from "./native-resource-capture.js";
9
+
10
+ describe("native creative resource update status", () => {
11
+ afterEach(() => {
12
+ unregisterNativeResourceCaptureAdapter("slides", "deck");
13
+ });
14
+
15
+ it("parses only complete native artifact identities", () => {
16
+ expect(parseNativeCreativeArtifactKey("slides:deck:deck-1")).toEqual({
17
+ appId: "slides",
18
+ resourceType: "deck",
19
+ resourceId: "deck-1",
20
+ });
21
+ expect(parseNativeCreativeArtifactKey("slides:deck:")).toBeNull();
22
+ expect(parseNativeCreativeArtifactKey("not-native")).toBeNull();
23
+ });
24
+
25
+ it("checks a resource type in one bounded adapter call", async () => {
26
+ const listResourceVersions = vi.fn(async (resourceIds: readonly string[]) =>
27
+ resourceIds.map((resourceId) => ({
28
+ resourceId,
29
+ sourceModifiedAt:
30
+ resourceId === "deck-current"
31
+ ? "2026-07-17T10:00:00Z"
32
+ : "2026-07-18T10:00:00Z",
33
+ })),
34
+ );
35
+ registerNativeResourceCaptureAdapter({
36
+ appId: "slides",
37
+ resourceType: "deck",
38
+ listResourceVersions,
39
+ async capture() {
40
+ throw new Error("not used");
41
+ },
42
+ });
43
+
44
+ const statuses = await resolveNativeCreativeResourceUpdateStatuses([
45
+ {
46
+ key: "membership-current",
47
+ appId: "slides",
48
+ resourceType: "deck",
49
+ resourceId: "deck-current",
50
+ publishedSourceModifiedAt: "2026-07-17T10:00:00Z",
51
+ },
52
+ {
53
+ key: "membership-stale",
54
+ appId: "slides",
55
+ resourceType: "deck",
56
+ resourceId: "deck-stale",
57
+ publishedSourceModifiedAt: "2026-07-17T10:00:00Z",
58
+ },
59
+ ]);
60
+
61
+ expect(listResourceVersions).toHaveBeenCalledOnce();
62
+ expect(listResourceVersions).toHaveBeenCalledWith([
63
+ "deck-current",
64
+ "deck-stale",
65
+ ]);
66
+ expect(statuses.get("membership-current")?.state).toBe("current");
67
+ expect(statuses.get("membership-stale")).toEqual({
68
+ key: "membership-stale",
69
+ state: "update-available",
70
+ reference: {
71
+ appId: "slides",
72
+ resourceType: "deck",
73
+ resourceId: "deck-stale",
74
+ expectedUpdatedAt: "2026-07-18T10:00:00Z",
75
+ },
76
+ });
77
+ });
78
+
79
+ it("omits inaccessible rows and fails closed when the adapter read fails", async () => {
80
+ const listResourceVersions = vi
81
+ .fn()
82
+ .mockResolvedValueOnce([])
83
+ .mockRejectedValueOnce(new Error("database unavailable"));
84
+ registerNativeResourceCaptureAdapter({
85
+ appId: "slides",
86
+ resourceType: "deck",
87
+ listResourceVersions,
88
+ async capture() {
89
+ throw new Error("not used");
90
+ },
91
+ });
92
+ const reference = {
93
+ key: "membership-1",
94
+ appId: "slides",
95
+ resourceType: "deck",
96
+ resourceId: "deck-1",
97
+ publishedSourceModifiedAt: "2026-07-17T10:00:00Z",
98
+ };
99
+
100
+ expect(
101
+ await resolveNativeCreativeResourceUpdateStatuses([reference]),
102
+ ).toEqual(new Map());
103
+ expect(
104
+ await resolveNativeCreativeResourceUpdateStatuses([reference]),
105
+ ).toEqual(new Map());
106
+ });
107
+
108
+ it("rejects unbounded status batches", async () => {
109
+ await expect(
110
+ resolveNativeCreativeResourceUpdateStatuses(
111
+ Array.from({ length: 101 }, (_, index) => ({
112
+ key: `membership-${index}`,
113
+ appId: "slides",
114
+ resourceType: "deck",
115
+ resourceId: `deck-${index}`,
116
+ publishedSourceModifiedAt: "2026-07-17T10:00:00Z",
117
+ })),
118
+ ),
119
+ ).rejects.toThrow("limited to 100 resources");
120
+ });
121
+ });
@@ -0,0 +1,198 @@
1
+ import type { NormalizedContextItem } from "../types.js";
2
+
3
+ /** A native app resource reference supplied by a governed-context submission. */
4
+ export interface NativeCreativeResourceRef {
5
+ appId: string;
6
+ resourceType: string;
7
+ resourceId: string;
8
+ expectedUpdatedAt?: string;
9
+ }
10
+
11
+ export interface NativeResourceCaptureAdapter {
12
+ appId: string;
13
+ resourceType: string;
14
+ /**
15
+ * Reads only the version marker for resources visible to the active caller.
16
+ * The Library calls this once per resource type with at most 100 ids.
17
+ */
18
+ listResourceVersions?(resourceIds: readonly string[]): Promise<
19
+ Array<{
20
+ resourceId: string;
21
+ sourceModifiedAt: string | null;
22
+ }>
23
+ >;
24
+ capture(reference: NativeCreativeResourceRef): Promise<{
25
+ artifactKey: string;
26
+ source: {
27
+ name: string;
28
+ kind: "native-app";
29
+ externalRef?: string;
30
+ upstreamAccess?: "available" | "restricted" | "unknown";
31
+ containerOwnerVerifiedAt?: string;
32
+ /** Server-derived from the app's source resource; never client input. */
33
+ access?: {
34
+ visibility: "private" | "org" | "public";
35
+ canManage: boolean;
36
+ };
37
+ };
38
+ items: NormalizedContextItem[];
39
+ /** Persisted only as internal submission metadata; never action output. */
40
+ privateMetadata?: Record<string, unknown>;
41
+ }>;
42
+ }
43
+
44
+ const REGISTRY_KEY = "__agentNativeCreativeContextNativeCaptureAdapters__";
45
+ type Registry = Map<string, NativeResourceCaptureAdapter>;
46
+
47
+ export type NativeCreativeResourceUpdateState =
48
+ | "current"
49
+ | "update-available"
50
+ | "unknown";
51
+
52
+ export interface PublishedNativeCreativeResourceRef {
53
+ key: string;
54
+ appId: string;
55
+ resourceType: string;
56
+ resourceId: string;
57
+ publishedSourceModifiedAt?: string | null;
58
+ }
59
+
60
+ export interface ResolvedNativeCreativeResourceUpdateStatus {
61
+ key: string;
62
+ state: NativeCreativeResourceUpdateState;
63
+ reference: NativeCreativeResourceRef;
64
+ }
65
+
66
+ const MAX_RESOURCE_VERSION_BATCH = 100;
67
+
68
+ function registry(): Registry {
69
+ const globalStore = globalThis as typeof globalThis & {
70
+ [REGISTRY_KEY]?: Registry;
71
+ };
72
+ return (globalStore[REGISTRY_KEY] ??= new Map());
73
+ }
74
+
75
+ function key(appId: string, resourceType: string): string {
76
+ return `${appId}:${resourceType}`;
77
+ }
78
+
79
+ export function registerNativeResourceCaptureAdapter(
80
+ adapter: NativeResourceCaptureAdapter,
81
+ ): () => void {
82
+ const adapterKey = key(adapter.appId, adapter.resourceType);
83
+ registry().set(adapterKey, adapter);
84
+ return () => registry().delete(adapterKey);
85
+ }
86
+
87
+ export function unregisterNativeResourceCaptureAdapter(
88
+ appId: string,
89
+ resourceType: string,
90
+ ): void {
91
+ registry().delete(key(appId, resourceType));
92
+ }
93
+
94
+ export async function captureNativeCreativeResource(
95
+ reference: NativeCreativeResourceRef,
96
+ ) {
97
+ const adapter = registry().get(key(reference.appId, reference.resourceType));
98
+ if (!adapter) {
99
+ throw new Error(
100
+ `No native creative-resource capture adapter is registered for ${reference.appId}/${reference.resourceType}`,
101
+ );
102
+ }
103
+ return adapter.capture(reference);
104
+ }
105
+
106
+ export function parseNativeCreativeArtifactKey(
107
+ artifactKey: string,
108
+ ): Omit<NativeCreativeResourceRef, "expectedUpdatedAt"> | null {
109
+ const firstSeparator = artifactKey.indexOf(":");
110
+ const secondSeparator = artifactKey.indexOf(":", firstSeparator + 1);
111
+ if (firstSeparator <= 0 || secondSeparator <= firstSeparator + 1) return null;
112
+ const appId = artifactKey.slice(0, firstSeparator);
113
+ const resourceType = artifactKey.slice(firstSeparator + 1, secondSeparator);
114
+ const resourceId = artifactKey.slice(secondSeparator + 1);
115
+ return resourceId ? { appId, resourceType, resourceId } : null;
116
+ }
117
+
118
+ /**
119
+ * Resolves update availability in bounded batches. Missing rows are omitted so
120
+ * an inaccessible native resource is indistinguishable from a deleted one.
121
+ */
122
+ export async function resolveNativeCreativeResourceUpdateStatuses(
123
+ references: readonly PublishedNativeCreativeResourceRef[],
124
+ ): Promise<Map<string, ResolvedNativeCreativeResourceUpdateStatus>> {
125
+ if (references.length > MAX_RESOURCE_VERSION_BATCH) {
126
+ throw new Error(
127
+ `Native creative-resource status checks are limited to ${MAX_RESOURCE_VERSION_BATCH} resources`,
128
+ );
129
+ }
130
+
131
+ const groups = new Map<
132
+ string,
133
+ {
134
+ adapter: NativeResourceCaptureAdapter;
135
+ references: PublishedNativeCreativeResourceRef[];
136
+ }
137
+ >();
138
+ for (const reference of references) {
139
+ const adapter = registry().get(
140
+ key(reference.appId, reference.resourceType),
141
+ );
142
+ if (!adapter?.listResourceVersions) continue;
143
+ const adapterKey = key(reference.appId, reference.resourceType);
144
+ const group = groups.get(adapterKey) ?? { adapter, references: [] };
145
+ group.references.push(reference);
146
+ groups.set(adapterKey, group);
147
+ }
148
+
149
+ const resolved = new Map<
150
+ string,
151
+ ResolvedNativeCreativeResourceUpdateStatus
152
+ >();
153
+ await Promise.all(
154
+ [...groups.values()].map(
155
+ async ({ adapter, references: groupReferences }) => {
156
+ try {
157
+ const resourceIds = [
158
+ ...new Set(groupReferences.map((item) => item.resourceId)),
159
+ ];
160
+ const versions = await adapter.listResourceVersions!(resourceIds);
161
+ const versionByResourceId = new Map(
162
+ versions.map((version) => [
163
+ version.resourceId,
164
+ version.sourceModifiedAt,
165
+ ]),
166
+ );
167
+ for (const reference of groupReferences) {
168
+ if (!versionByResourceId.has(reference.resourceId)) continue;
169
+ const sourceModifiedAt =
170
+ versionByResourceId.get(reference.resourceId) ?? null;
171
+ const state: NativeCreativeResourceUpdateState =
172
+ !sourceModifiedAt || !reference.publishedSourceModifiedAt
173
+ ? "unknown"
174
+ : sourceModifiedAt === reference.publishedSourceModifiedAt
175
+ ? "current"
176
+ : "update-available";
177
+ resolved.set(reference.key, {
178
+ key: reference.key,
179
+ state,
180
+ reference: {
181
+ appId: reference.appId,
182
+ resourceType: reference.resourceType,
183
+ resourceId: reference.resourceId,
184
+ ...(sourceModifiedAt
185
+ ? { expectedUpdatedAt: sourceModifiedAt }
186
+ : {}),
187
+ },
188
+ });
189
+ }
190
+ } catch {
191
+ // Update status is optional metadata; a native app read must not make
192
+ // the governed Library itself unavailable.
193
+ }
194
+ },
195
+ ),
196
+ );
197
+ return resolved;
198
+ }
@@ -32,6 +32,7 @@ describe("creative context public serialization", () => {
32
32
  token: "secret-token",
33
33
  values: [PRIVATE_HANDLE, "safe"],
34
34
  },
35
+ nativeClone: { handle: "opaque-native-handle" },
35
36
  });
36
37
  expect(result).toEqual({ safe: "caption", nested: { values: ["safe"] } });
37
38
  });
@@ -8,7 +8,7 @@ import type {
8
8
  } from "../types.js";
9
9
 
10
10
  const BLOCKED_METADATA_KEY =
11
- /(?:authorization|token|secret|password|cookie|storageKey|blobRef|rawSnapshot|downloadUrl|providerUrl|leaseToken|leaseOwner)/i;
11
+ /(?:authorization|token|secret|password|cookie|storageKey|blobRef|rawSnapshot|downloadUrl|providerUrl|leaseToken|leaseOwner|handle|cloneReference|privateMetadata|nativeClone)/i;
12
12
  const CAPABILITY_VALUE =
13
13
  /(?:creative-context-blob:|private-blob:|https?:\/\/)[^\s<>{}\[\]"']*/gi;
14
14
 
@@ -47,6 +47,7 @@ export interface CreativeContextSearchInput {
47
47
  mediaId?: string;
48
48
  sourceIds?: string[];
49
49
  packId?: string;
50
+ contextId?: string;
50
51
  kinds?: string[];
51
52
  tags?: string[];
52
53
  colors?: string[];
@@ -184,6 +185,7 @@ export async function performCreativeContextSearch(
184
185
  const filters = {
185
186
  sourceIds: input.sourceIds,
186
187
  packId: input.packId,
188
+ contextId: input.contextId,
187
189
  kinds: input.kinds,
188
190
  tags: input.tags,
189
191
  colors: input.colors,
@@ -461,6 +463,7 @@ export async function performCreativeContextSearch(
461
463
  filters: {
462
464
  sourceIds: input.sourceIds,
463
465
  packId: input.packId,
466
+ contextId: input.contextId,
464
467
  kinds: input.kinds,
465
468
  tags: input.tags,
466
469
  colors: input.colors,
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { sanitizeSafeNativePreviewHtml } from "./safe-native-preview.js";
4
+
5
+ describe("safe native preview sanitizer", () => {
6
+ it("removes executable markup, framework handlers, and remote subresources", () => {
7
+ const sanitized = sanitizeSafeNativePreviewHtml(`
8
+ <script>alert(1)</script>
9
+ <iframe src="https://private.example"></iframe>
10
+ <link rel="stylesheet" href="https://private.example/style.css">
11
+ <style>@import "https://private.example/theme.css"; .hero { background: url(https://private.example/a.png) }</style>
12
+ <div onclick="steal()" x-data="{}">
13
+ <img src="https://private.example/image.png" srcset="https://private.example/2x.png 2x">
14
+ </div>
15
+ `);
16
+
17
+ expect(sanitized).not.toMatch(
18
+ /script|iframe|private\.example|onclick|srcset|x-data/i,
19
+ );
20
+ });
21
+
22
+ it("keeps inert inline image data for self-contained snapshots", () => {
23
+ const sanitized = sanitizeSafeNativePreviewHtml(
24
+ '<img src="data:image/png;base64,AAAA"><a href="#detail">Detail</a>',
25
+ );
26
+ expect(sanitized).toContain("data:image/png;base64,AAAA");
27
+ expect(sanitized).toContain('href="#detail"');
28
+ });
29
+ });
@@ -0,0 +1,200 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+
4
+ const MAX_PREVIEW_HTML_BYTES = 512 * 1024;
5
+ const MAX_PREVIEW_COUNT = 12;
6
+ const MAX_PREVIEW_DIMENSION = 1_600;
7
+ const MAX_PREVIEW_PNG_BYTES = 4 * 1024 * 1024;
8
+
9
+ export interface SafeNativeHtmlPreviewInput {
10
+ id: string;
11
+ html: string;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ export interface SafeNativeHtmlPreviewResult {
17
+ id: string;
18
+ data: Uint8Array;
19
+ width: number;
20
+ height: number;
21
+ }
22
+
23
+ type PlaywrightModule = {
24
+ chromium: import("playwright").BrowserType;
25
+ };
26
+
27
+ const SYSTEM_CHROME_EXECUTABLES = [
28
+ "/usr/bin/google-chrome-stable",
29
+ "/usr/bin/google-chrome",
30
+ "/usr/bin/chromium-browser",
31
+ "/usr/bin/chromium",
32
+ ];
33
+
34
+ function safeDimension(value: number) {
35
+ return Math.max(200, Math.min(MAX_PREVIEW_DIMENSION, Math.round(value)));
36
+ }
37
+
38
+ export function sanitizeSafeNativePreviewHtml(html: string): string {
39
+ return html
40
+ .replace(
41
+ /<(script|noscript|iframe|object|embed|form|meta|base|link)\b[\s\S]*?<\/\1>/gi,
42
+ "",
43
+ )
44
+ .replace(
45
+ /<(script|noscript|iframe|object|embed|form|meta|base|link)\b[^>]*\/?>/gi,
46
+ "",
47
+ )
48
+ .replace(/\s+on[a-z][\w:-]*\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "")
49
+ .replace(
50
+ /\s+(?:x-[\w:-]+|@[\w:-]+|:[\w:-]+)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
51
+ "",
52
+ )
53
+ .replace(/\s+(?:srcdoc|srcset)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "")
54
+ .replace(
55
+ /\s+(href|src|poster|action)\s*=\s*(["'])(?!\s*(?:data:image\/(?:png|jpe?g|gif|webp|svg\+xml);|#))[^"']*\2/gi,
56
+ "",
57
+ )
58
+ .replace(
59
+ /\s+(?:href|src|poster|action)\s*=\s*(?!["'])(?!data:image\/(?:png|jpe?g|gif|webp|svg\+xml);|#)[^\s>]+/gi,
60
+ "",
61
+ )
62
+ .replace(/@import\s+(?:url\()?\s*["']?[^;"')]+["']?\s*\)?\s*;?/gi, "")
63
+ .replace(
64
+ /url\(\s*(["']?)(?!data:image\/(?:png|jpe?g|gif|webp|svg\+xml);)[^)]+\1\s*\)/gi,
65
+ "none",
66
+ );
67
+ }
68
+
69
+ async function importPlaywright(): Promise<PlaywrightModule> {
70
+ const specifier = "playwright";
71
+ return (await import(
72
+ /* @vite-ignore */ specifier
73
+ )) as unknown as PlaywrightModule;
74
+ }
75
+
76
+ function isMissingBrowserError(error: unknown) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(
79
+ message,
80
+ );
81
+ }
82
+
83
+ async function launchBrowser(chromium: import("playwright").BrowserType) {
84
+ const launchOptions = {
85
+ headless: true as const,
86
+ args: ["--no-sandbox", "--disable-dev-shm-usage"],
87
+ };
88
+ try {
89
+ return await chromium.launch(launchOptions);
90
+ } catch (error) {
91
+ if (!isMissingBrowserError(error)) throw error;
92
+ for (const executablePath of SYSTEM_CHROME_EXECUTABLES) {
93
+ if (!existsSync(executablePath)) continue;
94
+ try {
95
+ return await chromium.launch({ ...launchOptions, executablePath });
96
+ } catch {
97
+ continue;
98
+ }
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ async function connectHostedBrowser(
105
+ chromium: import("playwright").BrowserType,
106
+ ): Promise<import("playwright").Browser> {
107
+ const server = (await import("@agent-native/core/server")) as unknown as {
108
+ requestBuilderBrowserConnection?: (input: {
109
+ sessionId: string;
110
+ }) => Promise<Record<string, unknown>>;
111
+ };
112
+ if (!server.requestBuilderBrowserConnection) {
113
+ throw new Error("Builder Browser is unavailable.");
114
+ }
115
+ const connection = await server.requestBuilderBrowserConnection({
116
+ sessionId: `creative-context-preview-${randomUUID()}`,
117
+ });
118
+ const wsUrl = typeof connection.wsUrl === "string" ? connection.wsUrl : "";
119
+ if (!wsUrl) throw new Error("Builder Browser did not return a connection.");
120
+ return chromium.connectOverCDP(wsUrl);
121
+ }
122
+
123
+ export async function renderSafeNativeHtmlPreviews(
124
+ inputs: SafeNativeHtmlPreviewInput[],
125
+ ): Promise<SafeNativeHtmlPreviewResult[]> {
126
+ const bounded = inputs.slice(0, MAX_PREVIEW_COUNT).flatMap((input) => {
127
+ if (Buffer.byteLength(input.html, "utf8") > MAX_PREVIEW_HTML_BYTES) {
128
+ return [];
129
+ }
130
+ return [
131
+ {
132
+ ...input,
133
+ html: sanitizeSafeNativePreviewHtml(input.html),
134
+ width: safeDimension(input.width),
135
+ height: safeDimension(input.height),
136
+ },
137
+ ];
138
+ });
139
+ if (!bounded.length) return [];
140
+
141
+ let browser: import("playwright").Browser | undefined;
142
+ try {
143
+ const playwright = await importPlaywright();
144
+ browser = await connectHostedBrowser(playwright.chromium).catch(() =>
145
+ launchBrowser(playwright.chromium),
146
+ );
147
+ } catch {
148
+ return [];
149
+ }
150
+
151
+ const results: SafeNativeHtmlPreviewResult[] = [];
152
+ try {
153
+ for (const input of bounded) {
154
+ const context = await browser.newContext({
155
+ javaScriptEnabled: false,
156
+ serviceWorkers: "block",
157
+ viewport: { width: input.width, height: input.height },
158
+ });
159
+ try {
160
+ await context.route("**/*", async (route) => {
161
+ const protocol = new URL(route.request().url()).protocol;
162
+ if (
163
+ protocol === "about:" ||
164
+ protocol === "data:" ||
165
+ protocol === "blob:"
166
+ ) {
167
+ await route.continue();
168
+ return;
169
+ }
170
+ await route.abort("blockedbyclient");
171
+ });
172
+ const page = await context.newPage();
173
+ await page.setContent(input.html, {
174
+ waitUntil: "domcontentloaded",
175
+ timeout: 5_000,
176
+ });
177
+ const png = await page.screenshot({
178
+ type: "png",
179
+ animations: "disabled",
180
+ timeout: 5_000,
181
+ });
182
+ if (png.byteLength <= MAX_PREVIEW_PNG_BYTES) {
183
+ results.push({
184
+ id: input.id,
185
+ data: new Uint8Array(png),
186
+ width: input.width,
187
+ height: input.height,
188
+ });
189
+ }
190
+ } catch {
191
+ continue;
192
+ } finally {
193
+ await context.close().catch(() => {});
194
+ }
195
+ }
196
+ } finally {
197
+ await browser.close().catch(() => {});
198
+ }
199
+ return results;
200
+ }