@gradial/aci 0.1.30 → 0.1.31

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.
Binary file
package/bin/aci.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
- import { existsSync } from 'node:fs';
3
+ import { chmodSync, existsSync } from 'node:fs';
4
4
  import { cp, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
5
5
  import { createRequire } from 'node:module';
6
6
  import { basename, dirname, join, relative, resolve } from 'node:path';
@@ -62,6 +62,9 @@ function resolveWorkspaceBinary() {
62
62
  }
63
63
 
64
64
  const binaryPath = resolveBundledBinary() ?? resolvePackagedBinary() ?? resolveWorkspaceBinary();
65
+ if (binaryPath) {
66
+ try { chmodSync(binaryPath, 0o755); } catch {}
67
+ }
65
68
  const fallback = binaryPath ? null : resolveGoFallback();
66
69
 
67
70
  if (!binaryPath && !fallback) {
@@ -5,6 +5,13 @@ export interface RouteEntry {
5
5
  payloadRef: string;
6
6
  digest: string;
7
7
  fragmentRefs?: string[];
8
+ variants?: Record<string, RouteVariantEntry>;
9
+ }
10
+ export interface RouteVariantEntry {
11
+ variantKey: string;
12
+ payloadRef: string;
13
+ digest?: string;
14
+ variantValues?: Record<string, string>;
8
15
  }
9
16
  export interface FragmentEntry {
10
17
  id: string;
@@ -10,6 +10,9 @@ export async function getPage(route = '', config = {}) {
10
10
  if (runtimeInput)
11
11
  return runtimeInput.page;
12
12
  const provider = await resolveProviderWithFallback(config);
13
+ const variantPage = await pageFromFunctionRef(provider);
14
+ if (variantPage !== null)
15
+ return variantPage;
13
16
  return await provider.getPage(route);
14
17
  }
15
18
  export async function getSiteConfig(config = {}) {
@@ -53,7 +56,7 @@ export async function getRenderInput(route = '/', config = {}) {
53
56
  // Cache shared resources (manifest, siteConfig) but not per-page content
54
57
  // Page-level caching is handled by CDN
55
58
  const [page, siteConfig] = await Promise.all([
56
- provider.getPage(route),
59
+ pageFromFunctionRef(provider).then((variantPage) => variantPage ?? provider.getPage(route)),
57
60
  getCachedSiteConfig(provider),
58
61
  ]);
59
62
  return {
@@ -73,6 +76,28 @@ export async function getPageRuntimeRenderInput() {
73
76
  return null;
74
77
  }
75
78
  }
79
+ // The personalization edge proxies framework-origin requests with
80
+ // X-Baremetal-Function-Ref naming the exact page payload its decision
81
+ // selected (base or variant). When present, the SSR render must use that
82
+ // payload — otherwise every visitor gets the base page and edge decisioning
83
+ // is cosmetic. The S3 provider only serves refs the release manifest
84
+ // declares.
85
+ const BAREMETAL_FUNCTION_REF_HEADER = 'x-baremetal-function-ref';
86
+ async function pageFromFunctionRef(provider) {
87
+ if (!(provider instanceof S3ContentProvider))
88
+ return null;
89
+ let ref = '';
90
+ try {
91
+ const current = await headers();
92
+ ref = (current.get(BAREMETAL_FUNCTION_REF_HEADER) || '').trim();
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ if (!ref)
98
+ return null;
99
+ return await provider.getPageByRef(ref);
100
+ }
76
101
  export { isPreviewMode } from './preview-mode.js';
77
102
  export async function routeFromNextParams(params) {
78
103
  if (!params)
@@ -16,6 +16,13 @@ export declare class S3ContentProvider implements ContentProvider {
16
16
  listRoutes(): Promise<RouteEntry[]>;
17
17
  getSiteConfig<T = unknown>(): Promise<T>;
18
18
  getPage<T = unknown>(route: string): Promise<T>;
19
+ /**
20
+ * getPageByRef serves the exact payload the edge selected (the
21
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
22
+ * release manifest declares are served — the header names published
23
+ * content, it cannot address arbitrary keys.
24
+ */
25
+ getPageByRef<T = unknown>(payloadRef: string): Promise<T>;
19
26
  getFragment<T = unknown>(id: string): Promise<T>;
20
27
  getLayout<T = unknown>(name: string): Promise<T>;
21
28
  manifest(): Promise<CompiledManifest>;
@@ -32,6 +32,26 @@ export class S3ContentProvider {
32
32
  throw new PageNotFoundError(route, error);
33
33
  }
34
34
  }
35
+ /**
36
+ * getPageByRef serves the exact payload the edge selected (the
37
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
38
+ * release manifest declares are served — the header names published
39
+ * content, it cannot address arbitrary keys.
40
+ */
41
+ async getPageByRef(payloadRef) {
42
+ const manifest = await this.manifest();
43
+ const known = Object.values(manifest.routes).some((entry) => entry.payloadRef === payloadRef ||
44
+ Object.values(entry.variants ?? {}).some((variant) => variant.payloadRef === payloadRef));
45
+ if (!known) {
46
+ throw new PageNotFoundError(payloadRef);
47
+ }
48
+ try {
49
+ return await this.getJSON(payloadRef);
50
+ }
51
+ catch (error) {
52
+ throw new PageNotFoundError(payloadRef, error);
53
+ }
54
+ }
35
55
  async getFragment(id) {
36
56
  const manifest = await this.manifest();
37
57
  const entry = manifest.fragments[id];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,9 +17,7 @@
17
17
  "build:storybook": "storybook build"
18
18
  },
19
19
  "dependencies": {
20
- "@c15t/nextjs": "^2.1.0",
21
20
  "@gradial/aci": "{{aciVersion}}",
22
- "js-yaml": "^4.1.1",
23
21
  "next": "^15.1.6",
24
22
  "react": "^19.0.0",
25
23
  "react-dom": "^19.0.0"
@@ -1,8 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Inter, Space_Grotesk } from 'next/font/google';
3
3
  import { DevRefresh } from '@gradial/aci/next';
4
- import { ConsentManager } from '@/app/_components/ConsentManager';
5
- import '@c15t/nextjs/styles.css';
6
4
  import '@/design-system/styles.css';
7
5
 
8
6
  const inter = Inter({
@@ -23,7 +21,7 @@ export default function Layout({ children }: { children: ReactNode }) {
23
21
  return (
24
22
  <html lang="en" className={`${inter.variable} ${spaceGrotesk.variable}`}>
25
23
  <body>
26
- <ConsentManager>{children}</ConsentManager>
24
+ {children}
27
25
  <DevRefresh />
28
26
  </body>
29
27
  </html>
@@ -1,132 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- ConsentBanner,
5
- ConsentDialog,
6
- ConsentManagerProvider,
7
- useConsentManager,
8
- type ConsentManagerProviderProps,
9
- } from '@c15t/nextjs';
10
- import { useEffect, useMemo, type ReactNode } from 'react';
11
-
12
- // Hosted c15t backend (consent.io or self-hosted). Unset = offline mode:
13
- // consent is stored client-side and no network calls are made.
14
- const C15T_BACKEND_URL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
15
-
16
- // The CMP's own categories. The mapping from categories to the Gradial
17
- // personalization purpose lives in .content/config/site.json
18
- // (personalization.consentCategories), not here.
19
- const CONSENT_CATEGORIES = ['necessary', 'measurement', 'marketing'] as const;
20
-
21
- // ---------------------------------------------------------------------------
22
- // Gradial collect bridge
23
- //
24
- // The Gradial collect SDK's c15t preset (the site config's
25
- // `personalization.consentPreset: "c15t"`, emitted as
26
- // `data-aci-consent="c15t"` on the collect tag) reads
27
- // `window.c15t = { consents, onConsentChange }`. c15t itself does not
28
- // publish that shape, so this bridge mirrors the store onto it — only after
29
- // a real visitor decision (`consentInfo` set), never for pre-decision
30
- // defaults, so the SDK cannot mistake an untouched banner for a withdrawal.
31
- // ---------------------------------------------------------------------------
32
-
33
- type Consents = Record<string, boolean>;
34
- type ConsentListener = (payload: { consents: Consents }) => void;
35
-
36
- declare global {
37
- interface Window {
38
- c15t?: {
39
- consents?: Consents;
40
- onConsentChange?: (cb: ConsentListener) => void;
41
- };
42
- }
43
- }
44
-
45
- const consentListeners: ConsentListener[] = [];
46
-
47
- function publishAciConsents(consents: Consents): void {
48
- const bridge = (window.c15t ??= {});
49
- bridge.consents = consents;
50
- bridge.onConsentChange ??= (cb: ConsentListener) => {
51
- consentListeners.push(cb);
52
- };
53
- for (const listener of [...consentListeners]) {
54
- listener({ consents });
55
- }
56
- }
57
-
58
- function AciConsentBridge() {
59
- const { consents, consentInfo } = useConsentManager();
60
- useEffect(() => {
61
- if (!consentInfo || !consents) return;
62
- publishAciConsents(consents as Consents);
63
- }, [consents, consentInfo]);
64
- return null;
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Consent manager
69
- // ---------------------------------------------------------------------------
70
-
71
- function getConsentOptions(): ConsentManagerProviderProps['options'] {
72
- const baseOptions = {
73
- consentCategories: [...CONSENT_CATEGORIES],
74
- legalLinks: {
75
- privacyPolicy: {
76
- href: '/privacy/',
77
- target: '_self' as const,
78
- label: 'Privacy policy',
79
- },
80
- },
81
- };
82
-
83
- if (C15T_BACKEND_URL) {
84
- return {
85
- ...baseOptions,
86
- mode: 'hosted',
87
- backendURL: C15T_BACKEND_URL,
88
- };
89
- }
90
-
91
- return {
92
- ...baseOptions,
93
- mode: 'offline',
94
- offlinePolicy: {
95
- policyPacks: [
96
- {
97
- id: 'default-opt-in',
98
- match: { isDefault: true },
99
- consent: {
100
- model: 'opt-in',
101
- categories: [...CONSENT_CATEGORIES],
102
- },
103
- ui: { mode: 'banner' },
104
- },
105
- ],
106
- },
107
- };
108
- }
109
-
110
- export function ConsentManager({ children }: { children: ReactNode }) {
111
- const options = useMemo(() => getConsentOptions(), []);
112
-
113
- return (
114
- <ConsentManagerProvider options={options}>
115
- {children}
116
- <AciConsentBridge />
117
- <ConsentBanner
118
- title="Cookie preferences"
119
- description="We use cookies to keep the site working, measure performance, and support marketing."
120
- acceptButtonText="Accept all"
121
- rejectButtonText="Reject all"
122
- customizeButtonText="Customize"
123
- legalLinks={['privacyPolicy']}
124
- layout={[['reject', 'accept'], 'customize']}
125
- primaryButton="accept"
126
- scrollLock={false}
127
- hideBranding
128
- />
129
- <ConsentDialog legalLinks={['privacyPolicy']} scrollLock trapFocus hideBranding />
130
- </ConsentManagerProvider>
131
- );
132
- }