@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.
- package/README.md +51 -9
- package/agent-native.package.json +4 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-context-memberships.d.ts +11 -0
- package/dist/actions/list-context-memberships.d.ts.map +1 -0
- package/dist/actions/list-context-memberships.js +47 -0
- package/dist/actions/list-context-memberships.js.map +1 -0
- package/dist/actions/list-creative-contexts.d.ts +12 -0
- package/dist/actions/list-creative-contexts.d.ts.map +1 -0
- package/dist/actions/list-creative-contexts.js +28 -0
- package/dist/actions/list-creative-contexts.js.map +1 -0
- package/dist/actions/manage-context-membership.d.ts +64 -0
- package/dist/actions/manage-context-membership.d.ts.map +1 -0
- package/dist/actions/manage-context-membership.js +70 -0
- package/dist/actions/manage-context-membership.js.map +1 -0
- package/dist/actions/manage-context-pack.d.ts +3 -0
- package/dist/actions/manage-context-pack.js +6 -0
- package/dist/actions/manage-context-pack.js.map +1 -1
- package/dist/actions/manage-context-source.js +4 -11
- package/dist/actions/manage-context-source.js.map +1 -1
- package/dist/actions/manage-creative-context.d.ts +28 -0
- package/dist/actions/manage-creative-context.d.ts.map +1 -0
- package/dist/actions/manage-creative-context.js +65 -0
- package/dist/actions/manage-creative-context.js.map +1 -0
- package/dist/actions/process-context-purge.d.ts +16 -1
- package/dist/actions/process-context-purge.js +2 -1
- package/dist/actions/process-context-purge.js.map +1 -1
- package/dist/actions/search-creative-context.d.ts +1 -0
- package/dist/actions/search-creative-context.js +2 -1
- package/dist/actions/search-creative-context.js.map +1 -1
- package/dist/client/CreativeContextChip.d.ts +5 -1
- package/dist/client/CreativeContextChip.d.ts.map +1 -1
- package/dist/client/CreativeContextChip.js +36 -7
- package/dist/client/CreativeContextChip.js.map +1 -1
- package/dist/client/CreativeContextPanel.d.ts.map +1 -1
- package/dist/client/CreativeContextPanel.js +406 -89
- package/dist/client/CreativeContextPanel.js.map +1 -1
- package/dist/client/CreativeContextShareTab.d.ts +56 -0
- package/dist/client/CreativeContextShareTab.d.ts.map +1 -0
- package/dist/client/CreativeContextShareTab.js +213 -0
- package/dist/client/CreativeContextShareTab.js.map +1 -0
- package/dist/client/actions.d.ts +210 -0
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +318 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/application-state.d.ts +2 -0
- package/dist/client/application-state.d.ts.map +1 -1
- package/dist/client/application-state.js +25 -2
- package/dist/client/application-state.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +77 -0
- package/dist/client/messages.js.map +1 -1
- package/dist/connectors/types.d.ts +2 -2
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1592 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +95 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +80 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/server/generation-artifact-access.js +2 -0
- package/dist/server/generation-artifact-access.js.map +1 -1
- package/dist/server/generation-context.d.ts +12 -4
- package/dist/server/generation-context.d.ts.map +1 -1
- package/dist/server/generation-context.js +129 -7
- package/dist/server/generation-context.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/isolated-a2a.d.ts +3 -0
- package/dist/server/isolated-a2a.d.ts.map +1 -1
- package/dist/server/isolated-a2a.js +3 -1
- package/dist/server/isolated-a2a.js.map +1 -1
- package/dist/server/media.d.ts +1 -1
- package/dist/server/media.d.ts.map +1 -1
- package/dist/server/media.js +24 -9
- package/dist/server/media.js.map +1 -1
- package/dist/server/native-resource-capture.d.ts +78 -0
- package/dist/server/native-resource-capture.d.ts.map +1 -0
- package/dist/server/native-resource-capture.js +94 -0
- package/dist/server/native-resource-capture.js.map +1 -0
- package/dist/server/public-serialization.js +1 -1
- package/dist/server/public-serialization.js.map +1 -1
- package/dist/server/retrieval.d.ts +1 -0
- package/dist/server/retrieval.d.ts.map +1 -1
- package/dist/server/retrieval.js +2 -0
- package/dist/server/retrieval.js.map +1 -1
- package/dist/server/safe-native-preview.d.ts +15 -0
- package/dist/server/safe-native-preview.d.ts.map +1 -0
- package/dist/server/safe-native-preview.js +149 -0
- package/dist/server/safe-native-preview.js.map +1 -0
- package/dist/store/brand.d.ts.map +1 -1
- package/dist/store/brand.js +2 -16
- package/dist/store/brand.js.map +1 -1
- package/dist/store/content.d.ts +1 -0
- package/dist/store/content.d.ts.map +1 -1
- package/dist/store/content.js +84 -20
- package/dist/store/content.js.map +1 -1
- package/dist/store/contexts.d.ts +216 -0
- package/dist/store/contexts.d.ts.map +1 -0
- package/dist/store/contexts.js +1341 -0
- package/dist/store/contexts.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/packs.d.ts +3 -0
- package/dist/store/packs.d.ts.map +1 -1
- package/dist/store/packs.js +126 -9
- package/dist/store/packs.js.map +1 -1
- package/dist/store/purge.d.ts.map +1 -1
- package/dist/store/purge.js +103 -6
- package/dist/store/purge.js.map +1 -1
- package/dist/store/sources.d.ts.map +1 -1
- package/dist/store/sources.js +2 -15
- package/dist/store/sources.js.map +1 -1
- package/dist/store/suggestions.d.ts.map +1 -1
- package/dist/store/suggestions.js +3 -1
- package/dist/store/suggestions.js.map +1 -1
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/docs/skills/creative-context/SKILL.md +13 -5
- package/package.json +3 -3
- package/src/actions/index.ts +8 -0
- package/src/actions/job-public-serialization.spec.ts +21 -0
- package/src/actions/list-context-memberships.ts +56 -0
- package/src/actions/list-creative-contexts.spec.ts +38 -0
- package/src/actions/list-creative-contexts.ts +32 -0
- package/src/actions/manage-context-membership.ts +80 -0
- package/src/actions/manage-context-pack.ts +6 -0
- package/src/actions/manage-context-source.ts +8 -11
- package/src/actions/manage-creative-context.ts +73 -0
- package/src/actions/process-context-purge.ts +4 -1
- package/src/actions/search-creative-context.ts +2 -1
- package/src/client/CreativeContextChip.spec.ts +32 -0
- package/src/client/CreativeContextChip.tsx +83 -14
- package/src/client/CreativeContextPanel.tsx +1947 -885
- package/src/client/CreativeContextShareTab.spec.ts +70 -0
- package/src/client/CreativeContextShareTab.tsx +657 -0
- package/src/client/actions.spec.ts +177 -0
- package/src/client/actions.ts +566 -0
- package/src/client/application-state.spec.ts +5 -0
- package/src/client/application-state.ts +30 -2
- package/src/client/index.ts +33 -0
- package/src/client/messages.ts +95 -0
- package/src/connectors/types.ts +6 -8
- package/src/eval/library-flow.acceptance.spec.tsx +1 -0
- package/src/schema/index.ts +116 -0
- package/src/schema/migrations.ts +80 -0
- package/src/server/generation-artifact-access.ts +1 -0
- package/src/server/generation-context-a2a.spec.ts +133 -0
- package/src/server/generation-context.ts +153 -7
- package/src/server/index.spec.ts +15 -0
- package/src/server/index.ts +16 -0
- package/src/server/isolated-a2a.ts +4 -1
- package/src/server/media.spec.ts +30 -2
- package/src/server/media.ts +31 -12
- package/src/server/native-resource-capture.spec.ts +121 -0
- package/src/server/native-resource-capture.ts +198 -0
- package/src/server/public-serialization.spec.ts +1 -0
- package/src/server/public-serialization.ts +1 -1
- package/src/server/retrieval.ts +3 -0
- package/src/server/safe-native-preview.spec.ts +29 -0
- package/src/server/safe-native-preview.ts +200 -0
- package/src/store/access.integration.spec.ts +1116 -0
- package/src/store/brand.ts +2 -21
- package/src/store/content.ts +217 -17
- package/src/store/contexts.ts +1826 -0
- package/src/store/index.ts +1 -0
- package/src/store/packs.ts +279 -24
- package/src/store/purge.ts +139 -6
- package/src/store/sources.ts +2 -18
- package/src/store/suggestions.ts +3 -1
- 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
|
|
package/src/server/retrieval.ts
CHANGED
|
@@ -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
|
+
}
|