@gradial/aci 0.1.41 → 0.1.42
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/CHANGELOG.md +9 -13
- package/README.md +4 -4
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/astro/page.d.ts +6 -0
- package/dist/astro/page.js +3 -2
- package/dist/cli/compile-registry.d.mts +2 -0
- package/dist/cli/compile-registry.mjs +759 -0
- package/dist/cli/materialize-template.d.mts +19 -0
- package/dist/cli/materialize-template.mjs +128 -0
- package/dist/cli/validate-content.d.mts +2 -0
- package/dist/cli/validate-content.mjs +426 -0
- package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
- package/dist/cli/verify-next-cdn-routes.mjs +47 -0
- package/dist/collect/assignments.d.ts +65 -0
- package/dist/collect/assignments.js +138 -0
- package/dist/collect/batcher.d.ts +21 -0
- package/dist/collect/batcher.js +10 -0
- package/dist/collect/declarative.d.ts +1 -1
- package/dist/collect/declarative.js +7 -1
- package/dist/collect/index.js +30 -4
- package/dist/collect/pageviews.d.ts +15 -3
- package/dist/collect/pageviews.js +28 -13
- package/dist/collect/transport.d.ts +2 -0
- package/dist/collect/transport.js +32 -0
- package/dist/consent-banner/index.d.ts +33 -0
- package/dist/consent-banner/index.js +99 -0
- package/dist/content/personalization.d.ts +35 -0
- package/dist/content/personalization.js +57 -0
- package/dist/content/types.d.ts +7 -1
- package/dist/edge/personalization-core.d.ts +11 -0
- package/dist/edge/personalization-core.js +265 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/next/assignments-script.d.ts +14 -0
- package/dist/next/assignments-script.js +17 -0
- package/dist/next/collect-route.d.ts +19 -0
- package/dist/next/collect-route.js +351 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/middleware.d.ts +15 -1
- package/dist/next/middleware.js +71 -27
- package/dist/next/page.js +2 -1
- package/dist/next/personalization-host.d.ts +118 -0
- package/dist/next/personalization-host.js +276 -0
- package/dist/next/server.js +6 -6
- package/dist/next/vercel-personalization.d.ts +3 -0
- package/dist/next/vercel-personalization.js +3 -0
- package/dist/next/vercel-s3-personalization.d.ts +58 -0
- package/dist/next/vercel-s3-personalization.js +220 -0
- package/dist/providers/s3.d.ts +1 -1
- package/dist/providers/s3.js +1 -1
- package/dist/react/VideoPlayer.d.ts +6 -6
- package/dist/react/index.d.ts +6 -6
- package/dist/types/page.d.ts +1 -1
- package/package.json +19 -1
- package/src/astro/ConsentBanner.astro +62 -0
- package/src/cli/verify-next-cdn-routes.mjs +5 -4
- package/src/consent-banner/styles.css +110 -0
- package/src/types/page.ts +1 -1
- package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
- package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/astro/starter-content/src/cms/registry.ts +3 -0
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
- package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
- package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
- package/templates/astro/template/src/pages/[...slug].astro +1 -0
- package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
- package/templates/common/.content/audiences/preference-red/_index.json +11 -0
- package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
- package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
- package/templates/common/.content/pages/home/_index.json +114 -4
- package/templates/common/.content/segments/preference-blue/_index.json +20 -0
- package/templates/common/.content/segments/preference-red/_index.json +20 -0
- package/templates/common/contracts/components/index.ts +21 -1
- package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
- package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
- package/templates/nextjs/starter-content/src/middleware.ts +11 -0
- package/templates/nextjs/template/src/app/collect/route.ts +18 -0
- package/templates/nextjs/template/src/middleware.ts +6 -2
- package/templates/astro/template/.aci.yaml +0 -28
- package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- package/templates/common/.content/config/robots.txt +0 -2
- package/templates/common/.content/config/site.json +0 -197
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
- package/templates/common/.content/fragments/navbar/_index.json +0 -25
- package/templates/common/.content/fragments/site-footer/_index.json +0 -62
- package/templates/common/.content/pages/about/_index.json +0 -51
- package/templates/nextjs/template/.aci.yaml +0 -26
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
2
|
+
import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
|
|
3
|
+
import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Node-only Vercel collection adapter for audience lake objects and signed
|
|
6
|
+
* claims. The EdgeRouter never receives this adapter or AWS credentials.
|
|
7
|
+
* Production construction exchanges the Vercel workload OIDC token for
|
|
8
|
+
* scoped AWS credentials; no long-lived AWS keys are read.
|
|
9
|
+
*/
|
|
10
|
+
export class VercelS3PersonalizationStore {
|
|
11
|
+
config;
|
|
12
|
+
client;
|
|
13
|
+
constructor(config, client) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
const s3Config = {
|
|
16
|
+
region: config.region,
|
|
17
|
+
endpoint: config.endpoint,
|
|
18
|
+
forcePathStyle: config.forcePathStyle,
|
|
19
|
+
...(config.roleArn ? { credentials: awsCredentialsProvider({
|
|
20
|
+
roleArn: config.roleArn,
|
|
21
|
+
clientConfig: { region: config.region },
|
|
22
|
+
}) } : {}),
|
|
23
|
+
};
|
|
24
|
+
this.client = client || new S3Client(s3Config);
|
|
25
|
+
}
|
|
26
|
+
async readClaims(siteId, namespace, identityHash) {
|
|
27
|
+
const key = joinKey(this.config.claimsKeyPrefix || 'audience/claims/v1', requiredSegment(identityHash, 'identity hash').slice(0, 2).toLowerCase(), requiredSegment(siteId, 'site id'), requiredSegment(namespace, 'identity namespace').toLowerCase(), `${requiredSegment(identityHash, 'identity hash').toLowerCase()}.json`);
|
|
28
|
+
const body = await this.readObject(this.config.eventsBucket, key);
|
|
29
|
+
if (body === null)
|
|
30
|
+
return null;
|
|
31
|
+
const envelope = JSON.parse(body);
|
|
32
|
+
if (envelope.schemaVersion !== 1 || !envelope.signedValue?.trim()) {
|
|
33
|
+
throw new Error(`invalid claims envelope at ${key}`);
|
|
34
|
+
}
|
|
35
|
+
if (envelope.expiresAt && Date.parse(envelope.expiresAt) <= Date.now())
|
|
36
|
+
return null;
|
|
37
|
+
return envelope.signedValue;
|
|
38
|
+
}
|
|
39
|
+
async appendEvent(key, record) {
|
|
40
|
+
const input = {
|
|
41
|
+
Bucket: this.config.eventsBucket,
|
|
42
|
+
Key: cleanKey(key),
|
|
43
|
+
Body: JSON.stringify(record),
|
|
44
|
+
ContentType: 'application/json',
|
|
45
|
+
IfNoneMatch: '*',
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
await this.client.send(new PutObjectCommand(input));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!isConditionalConflict(error))
|
|
52
|
+
throw error;
|
|
53
|
+
// S3 can report 409 for two concurrent conditional writes even when
|
|
54
|
+
// neither write has committed yet. Confirm the object before treating
|
|
55
|
+
// the conflict as the idempotent duplicate case.
|
|
56
|
+
try {
|
|
57
|
+
await this.client.send(new HeadObjectCommand({ Bucket: input.Bucket, Key: input.Key }));
|
|
58
|
+
}
|
|
59
|
+
catch (headError) {
|
|
60
|
+
throw new Error(`conditional event write conflicted before ${input.Key} was stored`, { cause: headError });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async putSuppression(key, record) {
|
|
65
|
+
await this.client.send(new PutObjectCommand({
|
|
66
|
+
Bucket: this.config.eventsBucket,
|
|
67
|
+
Key: cleanKey(key),
|
|
68
|
+
Body: JSON.stringify(record),
|
|
69
|
+
ContentType: 'application/json',
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
async readObject(bucket, key) {
|
|
73
|
+
try {
|
|
74
|
+
const output = await this.client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
75
|
+
if (!output.Body)
|
|
76
|
+
throw new Error(`empty object body at ${key}`);
|
|
77
|
+
if (typeof output.Body.transformToString === 'function')
|
|
78
|
+
return await output.Body.transformToString();
|
|
79
|
+
throw new Error(`unsupported object body at ${key}`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (isNotFound(error))
|
|
83
|
+
return null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Edge Config is the provider projection for immutable release routing state.
|
|
90
|
+
* This tiny store is safe in Next middleware: it only fetches the route
|
|
91
|
+
* projection and never reads S3, claims, or event data.
|
|
92
|
+
*/
|
|
93
|
+
export class VercelEdgeConfigRouteStore {
|
|
94
|
+
edgeConfig;
|
|
95
|
+
siteId;
|
|
96
|
+
constructor(edgeConfig, siteId) {
|
|
97
|
+
this.edgeConfig = edgeConfig;
|
|
98
|
+
this.siteId = siteId;
|
|
99
|
+
}
|
|
100
|
+
async readEdgeManifest(releaseId) {
|
|
101
|
+
const value = await getUncachedEdgeConfigValue(this.edgeConfig, edgeRoutesKey(this.siteId, releaseId));
|
|
102
|
+
if (!value)
|
|
103
|
+
return null;
|
|
104
|
+
if (typeof value === 'string')
|
|
105
|
+
return JSON.parse(value);
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function createVercelS3PersonalizationStore(config) {
|
|
110
|
+
return new VercelS3PersonalizationStore(config);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Builds the EdgeRouter configuration used by Next middleware. It deliberately
|
|
114
|
+
* has no AWS/S3 configuration: its only remote dependency is the provider's
|
|
115
|
+
* Edge Config projection.
|
|
116
|
+
*/
|
|
117
|
+
export function createVercelPersonalizationFromEnv(env = process.env) {
|
|
118
|
+
if (env.ACI_PERSONALIZATION_HOST_ENABLED !== 'true')
|
|
119
|
+
return undefined;
|
|
120
|
+
const siteId = requiredEnv(env, 'ACI_SITE_ID');
|
|
121
|
+
const edgeConfig = requiredEnv(env, 'EDGE_CONFIG');
|
|
122
|
+
const currentKID = env.ACI_IDENTITY_KID || env.AUDIENCE_IDENTITY_COOKIE_KID || 'v1';
|
|
123
|
+
const currentSecret = env.ACI_IDENTITY_SECRET || env.AUDIENCE_IDENTITY_COOKIE_SECRET || '';
|
|
124
|
+
if (!currentSecret)
|
|
125
|
+
throw new Error('ACI_IDENTITY_SECRET is required when ACI_PERSONALIZATION_HOST_ENABLED=true');
|
|
126
|
+
const previousKID = env.ACI_IDENTITY_KID_PREVIOUS || env.AUDIENCE_IDENTITY_COOKIE_KID_PREVIOUS || '';
|
|
127
|
+
const previousSecret = env.ACI_IDENTITY_SECRET_PREVIOUS || env.AUDIENCE_IDENTITY_COOKIE_SECRET_PREVIOUS || '';
|
|
128
|
+
if ((previousKID === '') !== (previousSecret === '')) {
|
|
129
|
+
throw new Error('previous identity kid and secret must be configured together');
|
|
130
|
+
}
|
|
131
|
+
const identityKeys = {
|
|
132
|
+
current: { kid: currentKID, secret: currentSecret },
|
|
133
|
+
...(previousKID ? { previous: { kid: previousKID, secret: previousSecret } } : {}),
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
siteId,
|
|
137
|
+
store: new VercelEdgeConfigRouteStore(edgeConfig, siteId),
|
|
138
|
+
identityKeys,
|
|
139
|
+
claimsKeys: {
|
|
140
|
+
[currentKID]: currentSecret,
|
|
141
|
+
...(previousKID ? { [previousKID]: previousSecret } : {}),
|
|
142
|
+
},
|
|
143
|
+
secureCookies: env.AUDIENCE_IDENTITY_COOKIE_SECURE !== 'false',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Builds the Node.js `/collect` configuration. This is the only Vercel
|
|
147
|
+
* application runtime that receives data-plane storage configuration. */
|
|
148
|
+
export function createVercelCollectionFromEnv(env = process.env) {
|
|
149
|
+
if (env.ACI_PERSONALIZATION_HOST_ENABLED !== 'true')
|
|
150
|
+
return undefined;
|
|
151
|
+
const siteId = requiredEnv(env, 'ACI_SITE_ID');
|
|
152
|
+
const region = env.AWS_REGION || env.AWS_DEFAULT_REGION || '';
|
|
153
|
+
if (!region)
|
|
154
|
+
throw new Error('AWS_REGION is required for the Vercel /collect route');
|
|
155
|
+
const currentKID = env.ACI_IDENTITY_KID || env.AUDIENCE_IDENTITY_COOKIE_KID || 'v1';
|
|
156
|
+
const currentSecret = env.ACI_IDENTITY_SECRET || env.AUDIENCE_IDENTITY_COOKIE_SECRET || '';
|
|
157
|
+
if (!currentSecret)
|
|
158
|
+
throw new Error('ACI_IDENTITY_SECRET is required for the Vercel /collect route');
|
|
159
|
+
const previousKID = env.ACI_IDENTITY_KID_PREVIOUS || env.AUDIENCE_IDENTITY_COOKIE_KID_PREVIOUS || '';
|
|
160
|
+
const previousSecret = env.ACI_IDENTITY_SECRET_PREVIOUS || env.AUDIENCE_IDENTITY_COOKIE_SECRET_PREVIOUS || '';
|
|
161
|
+
if ((previousKID === '') !== (previousSecret === '')) {
|
|
162
|
+
throw new Error('previous identity kid and secret must be configured together');
|
|
163
|
+
}
|
|
164
|
+
const identityKeys = {
|
|
165
|
+
current: { kid: currentKID, secret: currentSecret },
|
|
166
|
+
...(previousKID ? { previous: { kid: previousKID, secret: previousSecret } } : {}),
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
siteId,
|
|
170
|
+
store: createVercelS3PersonalizationStore({
|
|
171
|
+
region,
|
|
172
|
+
roleArn: env.AWS_ROLE_ARN?.trim() || undefined,
|
|
173
|
+
hostingBucket: requiredEnv(env, 'HOSTING_BUCKET'),
|
|
174
|
+
eventsBucket: requiredEnv(env, 'EVENTS_BUCKET'),
|
|
175
|
+
hostingKeyPrefix: env.HOSTING_KEY_PREFIX,
|
|
176
|
+
claimsKeyPrefix: env.AUDIENCE_CLAIMS_S3_PREFIX,
|
|
177
|
+
}),
|
|
178
|
+
identityKeys,
|
|
179
|
+
claimsKeys: {
|
|
180
|
+
[currentKID]: currentSecret,
|
|
181
|
+
...(previousKID ? { [previousKID]: previousSecret } : {}),
|
|
182
|
+
},
|
|
183
|
+
secureCookies: env.AUDIENCE_IDENTITY_COOKIE_SECURE !== 'false',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function requiredSegment(value, label) {
|
|
187
|
+
const clean = cleanKey(value);
|
|
188
|
+
if (!clean || clean.includes('/'))
|
|
189
|
+
throw new Error(`${label} is required and must be one path segment`);
|
|
190
|
+
return clean;
|
|
191
|
+
}
|
|
192
|
+
function requiredEnv(env, name) {
|
|
193
|
+
const value = env[name]?.trim() || '';
|
|
194
|
+
if (!value)
|
|
195
|
+
throw new Error(`${name} is required when ACI_PERSONALIZATION_HOST_ENABLED=true`);
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
function cleanKey(value) {
|
|
199
|
+
return (value || '').trim().replace(/^\/+|\/+$/g, '');
|
|
200
|
+
}
|
|
201
|
+
function joinKey(...parts) {
|
|
202
|
+
return parts.map(cleanKey).filter(Boolean).join('/');
|
|
203
|
+
}
|
|
204
|
+
function edgeRoutesKey(siteId, releaseId) {
|
|
205
|
+
return `edgeRoutes_${edgeKeySegment(siteId)}_${edgeKeySegment(releaseId)}`;
|
|
206
|
+
}
|
|
207
|
+
function edgeKeySegment(value) {
|
|
208
|
+
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
209
|
+
}
|
|
210
|
+
function isNotFound(error) {
|
|
211
|
+
const value = error;
|
|
212
|
+
return value?.$metadata?.httpStatusCode === 404 || value?.name === 'NoSuchKey' || value?.name === 'NotFound';
|
|
213
|
+
}
|
|
214
|
+
function isConditionalConflict(error) {
|
|
215
|
+
const value = error;
|
|
216
|
+
return value?.$metadata?.httpStatusCode === 409
|
|
217
|
+
|| value?.$metadata?.httpStatusCode === 412
|
|
218
|
+
|| value?.name === 'ConditionalRequestConflict'
|
|
219
|
+
|| value?.name === 'PreconditionFailed';
|
|
220
|
+
}
|
package/dist/providers/s3.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare class S3ContentProvider implements ContentProvider {
|
|
|
18
18
|
getPage<T = unknown>(route: string): Promise<T>;
|
|
19
19
|
/**
|
|
20
20
|
* getPageByRef serves the exact payload the edge selected (the
|
|
21
|
-
* X-
|
|
21
|
+
* X-Aci-Content-Ref contract): base page or variant. Only refs the
|
|
22
22
|
* release manifest declares are served — the header names published
|
|
23
23
|
* content, it cannot address arbitrary keys.
|
|
24
24
|
*/
|
package/dist/providers/s3.js
CHANGED
|
@@ -34,7 +34,7 @@ export class S3ContentProvider {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* getPageByRef serves the exact payload the edge selected (the
|
|
37
|
-
* X-
|
|
37
|
+
* X-Aci-Content-Ref contract): base page or variant. Only refs the
|
|
38
38
|
* release manifest declares are served — the header names published
|
|
39
39
|
* content, it cannot address arbitrary keys.
|
|
40
40
|
*/
|
|
@@ -16,12 +16,16 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
|
|
|
16
16
|
title: string;
|
|
17
17
|
rel?: string | undefined | undefined;
|
|
18
18
|
content?: string | undefined | undefined;
|
|
19
|
-
dir?: string | undefined | undefined;
|
|
20
|
-
hidden?: boolean | undefined | undefined;
|
|
21
19
|
slot?: string | undefined | undefined;
|
|
22
20
|
style?: import("react").CSSProperties | undefined;
|
|
21
|
+
dir?: string | undefined | undefined;
|
|
22
|
+
hidden?: boolean | undefined | undefined;
|
|
23
|
+
dangerouslySetInnerHTML?: {
|
|
24
|
+
__html: string | TrustedHTML;
|
|
25
|
+
} | undefined | undefined;
|
|
23
26
|
children?: import("react").ReactNode;
|
|
24
27
|
className?: string | undefined | undefined;
|
|
28
|
+
lang?: string | undefined | undefined;
|
|
25
29
|
crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
|
|
26
30
|
defaultChecked?: boolean | undefined | undefined;
|
|
27
31
|
defaultValue?: string | number | readonly string[] | undefined;
|
|
@@ -34,7 +38,6 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
|
|
|
34
38
|
contextMenu?: string | undefined | undefined;
|
|
35
39
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
36
40
|
enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
37
|
-
lang?: string | undefined | undefined;
|
|
38
41
|
nonce?: string | undefined | undefined;
|
|
39
42
|
spellCheck?: (boolean | "true" | "false") | undefined;
|
|
40
43
|
tabIndex?: number | undefined | undefined;
|
|
@@ -121,9 +124,6 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
|
|
|
121
124
|
"aria-valuemin"?: number | undefined | undefined;
|
|
122
125
|
"aria-valuenow"?: number | undefined | undefined;
|
|
123
126
|
"aria-valuetext"?: string | undefined | undefined;
|
|
124
|
-
dangerouslySetInnerHTML?: {
|
|
125
|
-
__html: string | TrustedHTML;
|
|
126
|
-
} | undefined | undefined;
|
|
127
127
|
onCopy?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
|
|
128
128
|
onCopyCapture?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
|
|
129
129
|
onCut?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
|
package/dist/react/index.d.ts
CHANGED
|
@@ -25,12 +25,16 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
|
|
|
25
25
|
rel?: string | undefined | undefined;
|
|
26
26
|
content?: string | undefined | undefined;
|
|
27
27
|
referrerPolicy?: import("react").HTMLAttributeReferrerPolicy | undefined;
|
|
28
|
-
dir?: string | undefined | undefined;
|
|
29
|
-
hidden?: boolean | undefined | undefined;
|
|
30
28
|
slot?: string | undefined | undefined;
|
|
31
29
|
style?: import("react").CSSProperties | undefined;
|
|
30
|
+
dir?: string | undefined | undefined;
|
|
31
|
+
hidden?: boolean | undefined | undefined;
|
|
32
|
+
dangerouslySetInnerHTML?: {
|
|
33
|
+
__html: string | TrustedHTML;
|
|
34
|
+
} | undefined | undefined;
|
|
32
35
|
children?: ReactNode;
|
|
33
36
|
className?: string | undefined | undefined;
|
|
37
|
+
lang?: string | undefined | undefined;
|
|
34
38
|
crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
|
|
35
39
|
fetchPriority?: "high" | "low" | "auto" | undefined | undefined;
|
|
36
40
|
useMap?: string | undefined | undefined;
|
|
@@ -45,7 +49,6 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
|
|
|
45
49
|
contextMenu?: string | undefined | undefined;
|
|
46
50
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
47
51
|
enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
48
|
-
lang?: string | undefined | undefined;
|
|
49
52
|
nonce?: string | undefined | undefined;
|
|
50
53
|
spellCheck?: (boolean | "true" | "false") | undefined;
|
|
51
54
|
tabIndex?: number | undefined | undefined;
|
|
@@ -132,9 +135,6 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
|
|
|
132
135
|
"aria-valuemin"?: number | undefined | undefined;
|
|
133
136
|
"aria-valuenow"?: number | undefined | undefined;
|
|
134
137
|
"aria-valuetext"?: string | undefined | undefined;
|
|
135
|
-
dangerouslySetInnerHTML?: {
|
|
136
|
-
__html: string | TrustedHTML;
|
|
137
|
-
} | undefined | undefined;
|
|
138
138
|
onCopy?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
|
|
139
139
|
onCopyCapture?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
|
|
140
140
|
onCut?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
|
package/dist/types/page.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"bin/aci.js",
|
|
10
10
|
"src/cli/*.mjs",
|
|
11
11
|
"src/types/*.ts",
|
|
12
|
+
"src/astro/*.astro",
|
|
13
|
+
"src/consent-banner/*.css",
|
|
12
14
|
"templates/common",
|
|
13
15
|
"templates/nextjs",
|
|
14
16
|
"templates/astro",
|
|
@@ -65,6 +67,11 @@
|
|
|
65
67
|
"import": "./dist/next/middleware.js",
|
|
66
68
|
"default": "./dist/next/middleware.js"
|
|
67
69
|
},
|
|
70
|
+
"./next/vercel-personalization": {
|
|
71
|
+
"types": "./dist/next/vercel-personalization.d.ts",
|
|
72
|
+
"import": "./dist/next/vercel-personalization.js",
|
|
73
|
+
"default": "./dist/next/vercel-personalization.js"
|
|
74
|
+
},
|
|
68
75
|
"./next/asset-route": {
|
|
69
76
|
"types": "./dist/next/asset-route.d.ts",
|
|
70
77
|
"import": "./dist/next/asset-route.js",
|
|
@@ -84,12 +91,21 @@
|
|
|
84
91
|
"import": "./dist/astro/index.js",
|
|
85
92
|
"default": "./dist/astro/index.js"
|
|
86
93
|
},
|
|
94
|
+
"./astro/ConsentBanner": {
|
|
95
|
+
"import": "./src/astro/ConsentBanner.astro"
|
|
96
|
+
},
|
|
97
|
+
"./consent-banner": {
|
|
98
|
+
"types": "./dist/consent-banner/index.d.ts",
|
|
99
|
+
"import": "./dist/consent-banner/index.js"
|
|
100
|
+
},
|
|
87
101
|
"./testing": {
|
|
88
102
|
"types": "./dist/testing/conformance/index.d.ts",
|
|
89
103
|
"import": "./dist/testing/conformance/index.js"
|
|
90
104
|
}
|
|
91
105
|
},
|
|
92
106
|
"dependencies": {
|
|
107
|
+
"@aws-sdk/client-s3": "^3.883.0",
|
|
108
|
+
"@vercel/oidc-aws-credentials-provider": "^3.3.0",
|
|
93
109
|
"markdown-it": "^14.1.0",
|
|
94
110
|
"sanitize-html": "^2.13.1",
|
|
95
111
|
"ws": "^8.18.0"
|
|
@@ -121,6 +137,7 @@
|
|
|
121
137
|
"@types/react": "^19.0.0",
|
|
122
138
|
"@types/sanitize-html": "^2.16.0",
|
|
123
139
|
"@types/ws": "^8.18.0",
|
|
140
|
+
"esbuild": "0.28.1",
|
|
124
141
|
"jsdom": "^29.1.1",
|
|
125
142
|
"next": "^15.5.19",
|
|
126
143
|
"tsx": "^4.20.0",
|
|
@@ -138,6 +155,7 @@
|
|
|
138
155
|
"scripts": {
|
|
139
156
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
140
157
|
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
158
|
+
"build:collect": "node scripts/build-collect.mjs",
|
|
141
159
|
"build:cli": "cd ../.. && ./scripts/build-cli.sh",
|
|
142
160
|
"generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
|
|
143
161
|
"compile-registry": "tsx src/cli/compile-registry.mjs"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
// ConsentBanner.astro — Astro wrapper around the SDK's first-party consent
|
|
3
|
+
// banner. Import this into your layout and pass site-specific text/styles.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// import ConsentBanner from '@gradial/aci/astro/ConsentBanner.astro';
|
|
7
|
+
// <ConsentBanner
|
|
8
|
+
// heading="Cookie Preferences"
|
|
9
|
+
// message="We use cookies to personalize your experience."
|
|
10
|
+
// acceptLabel="Accept all"
|
|
11
|
+
// rejectLabel="Reject"
|
|
12
|
+
// privacyPolicyHref="/privacy"
|
|
13
|
+
// privacyPolicyLabel="Privacy Policy"
|
|
14
|
+
// />
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
heading?: string;
|
|
18
|
+
message?: string;
|
|
19
|
+
acceptLabel?: string;
|
|
20
|
+
rejectLabel?: string;
|
|
21
|
+
privacyPolicyLabel?: string;
|
|
22
|
+
privacyPolicyHref?: string;
|
|
23
|
+
source?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
heading,
|
|
28
|
+
message,
|
|
29
|
+
acceptLabel,
|
|
30
|
+
rejectLabel,
|
|
31
|
+
privacyPolicyLabel,
|
|
32
|
+
privacyPolicyHref,
|
|
33
|
+
source,
|
|
34
|
+
} = Astro.props as Props;
|
|
35
|
+
|
|
36
|
+
const opts = JSON.stringify({
|
|
37
|
+
heading,
|
|
38
|
+
message,
|
|
39
|
+
acceptLabel,
|
|
40
|
+
rejectLabel,
|
|
41
|
+
privacyPolicyLabel,
|
|
42
|
+
privacyPolicyHref,
|
|
43
|
+
source,
|
|
44
|
+
});
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<div
|
|
48
|
+
id="aci-consent-banner-mount"
|
|
49
|
+
data-aci-consent-banner-mount
|
|
50
|
+
data-opts={opts}
|
|
51
|
+
>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
import { mountConsentBanner } from '../consent-banner/index.js';
|
|
56
|
+
|
|
57
|
+
const mount = document.getElementById('aci-consent-banner-mount');
|
|
58
|
+
if (mount) {
|
|
59
|
+
const opts = JSON.parse(mount.getAttribute('data-opts') || '{}');
|
|
60
|
+
mountConsentBanner(document, opts);
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
@@ -52,16 +52,17 @@ checkMissing(
|
|
|
52
52
|
'Use src/app/[[...slug]]/page.tsx (optional catch-all). The required [...slug] form breaks file-provider local dev where "/" arrives with an empty slug.',
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
'
|
|
55
|
+
checkFile(
|
|
56
|
+
'canonical asset route exists',
|
|
57
57
|
'src/app/gradial/assets/[...path]/route.ts',
|
|
58
|
-
|
|
58
|
+
(content) => content.includes('@gradial/aci/next/asset-route'),
|
|
59
|
+
'Expected the canonical asset route to use @gradial/aci/next/asset-route.',
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
checkMissing(
|
|
62
63
|
'legacy release-scoped asset route is removed',
|
|
63
64
|
'src/app/gradial/assets/[releaseId]/[...path]/route.ts',
|
|
64
|
-
'
|
|
65
|
+
'Release-scoped asset URLs are produced by withAci() rewrites into the canonical src/app/gradial/assets/[...path]/route.ts; do not add a [releaseId] route.',
|
|
65
66
|
);
|
|
66
67
|
|
|
67
68
|
checkMissing(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* Base consent banner styles — sites override with their own CSS. */
|
|
2
|
+
/* The banner is injected into document.body by mountConsentBanner(). */
|
|
3
|
+
|
|
4
|
+
#aci-consent-banner {
|
|
5
|
+
position: fixed;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
right: 0;
|
|
9
|
+
z-index: 9999;
|
|
10
|
+
background: #1a1a2e;
|
|
11
|
+
color: #fff;
|
|
12
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
16
|
+
opacity: 1;
|
|
17
|
+
transform: translateY(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#aci-consent-banner.aci-consent-banner--hidden {
|
|
21
|
+
opacity: 0;
|
|
22
|
+
transform: translateY(100%);
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#aci-consent-banner .aci-consent-banner__content {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
gap: 16px;
|
|
31
|
+
max-width: 1200px;
|
|
32
|
+
margin: 0 auto;
|
|
33
|
+
padding: 16px 24px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#aci-consent-banner .aci-consent-banner__text {
|
|
37
|
+
flex: 1;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#aci-consent-banner .aci-consent-banner__heading {
|
|
42
|
+
margin: 0 0 4px 0;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#aci-consent-banner .aci-consent-banner__message {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-size: 13px;
|
|
50
|
+
opacity: 0.85;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#aci-consent-banner .aci-consent-banner__privacy-link {
|
|
54
|
+
margin-left: 8px;
|
|
55
|
+
color: #8ecae6;
|
|
56
|
+
text-decoration: underline;
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#aci-consent-banner .aci-consent-banner__actions {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#aci-consent-banner .aci-consent-banner__btn {
|
|
67
|
+
padding: 8px 20px;
|
|
68
|
+
border: none;
|
|
69
|
+
border-radius: 6px;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
transition: background-color 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#aci-consent-banner .aci-consent-banner__btn--accept {
|
|
77
|
+
background: #2196f3;
|
|
78
|
+
color: #fff;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#aci-consent-banner .aci-consent-banner__btn--accept:hover {
|
|
82
|
+
background: #1976d2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#aci-consent-banner .aci-consent-banner__btn--reject {
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: #fff;
|
|
88
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#aci-consent-banner .aci-consent-banner__btn--reject:hover {
|
|
92
|
+
background: rgba(255, 255, 255, 0.1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (max-width: 640px) {
|
|
96
|
+
#aci-consent-banner .aci-consent-banner__content {
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
align-items: stretch;
|
|
99
|
+
gap: 12px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#aci-consent-banner .aci-consent-banner__actions {
|
|
103
|
+
flex-direction: column-reverse;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#aci-consent-banner .aci-consent-banner__btn {
|
|
107
|
+
width: 100%;
|
|
108
|
+
text-align: center;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/types/page.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { containerContract } from './container.contract';
|
|
|
3
3
|
import { featureCardContract } from './featureCard.contract';
|
|
4
4
|
import { featureGridContract } from './featureGrid.contract';
|
|
5
5
|
import { homeHeroContract } from './homeHero.contract';
|
|
6
|
+
import { personalizationChoiceContract } from './personalizationChoice.contract';
|
|
6
7
|
import { siteNavigationContract } from './siteNavigation.contract';
|
|
7
8
|
import { siteFooterContract } from './siteFooter.contract';
|
|
8
9
|
import { statsBarContract } from './statsBar.contract';
|
|
9
10
|
|
|
10
11
|
const componentContracts = [
|
|
11
12
|
homeHeroContract,
|
|
13
|
+
personalizationChoiceContract,
|
|
12
14
|
containerContract,
|
|
13
15
|
buttonContract,
|
|
14
16
|
siteNavigationContract,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineComponentContract } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
title: z.string().min(1),
|
|
6
|
+
description: z.string().optional(),
|
|
7
|
+
redLabel: z.string().min(1),
|
|
8
|
+
blueLabel: z.string().min(1),
|
|
9
|
+
refreshLabel: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
13
|
+
|
|
14
|
+
export const personalizationChoiceContract = defineComponentContract({
|
|
15
|
+
id: 'personalization_choice',
|
|
16
|
+
props: schema,
|
|
17
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRegistry } from '@gradial/aci';
|
|
2
2
|
import { homeHeroContract } from './contracts/components/homeHero.contract';
|
|
3
|
+
import { personalizationChoiceContract } from './contracts/components/personalizationChoice.contract';
|
|
3
4
|
import { containerContract } from './contracts/components/container.contract';
|
|
4
5
|
import { buttonContract } from './contracts/components/button.contract';
|
|
5
6
|
import { siteNavigationContract } from './contracts/components/siteNavigation.contract';
|
|
@@ -8,6 +9,7 @@ import { featureGridContract } from './contracts/components/featureGrid.contract
|
|
|
8
9
|
import { featureCardContract } from './contracts/components/featureCard.contract';
|
|
9
10
|
import { statsBarContract } from './contracts/components/statsBar.contract';
|
|
10
11
|
import HomeHero from '../components/sections/HomeHero.astro';
|
|
12
|
+
import PersonalizationChoice from '../components/sections/PersonalizationChoice.astro';
|
|
11
13
|
import ContainerSection from '../components/sections/ContainerSection.astro';
|
|
12
14
|
import ButtonBlock from '../components/elements/ButtonBlock.astro';
|
|
13
15
|
import Header from '../components/chrome/Header.astro';
|
|
@@ -18,6 +20,7 @@ import StatsBar from '../components/sections/StatsBar.astro';
|
|
|
18
20
|
|
|
19
21
|
export const registry = createRegistry([
|
|
20
22
|
[homeHeroContract, HomeHero],
|
|
23
|
+
[personalizationChoiceContract, PersonalizationChoice],
|
|
21
24
|
[containerContract, ContainerSection],
|
|
22
25
|
[buttonContract, ButtonBlock],
|
|
23
26
|
[siteNavigationContract, Header],
|