@preprio/toolkit 0.6.0-alpha.6 → 0.6.0-alpha.7
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 +138 -52
- package/dist/astro/{chunk-MWRA3732.js → chunk-3EXK2LHI.js} +1 -1
- package/dist/astro/graphql.cjs +58 -10
- package/dist/astro/graphql.d.cts +33 -38
- package/dist/astro/graphql.d.ts +33 -38
- package/dist/astro/graphql.js +59 -11
- package/dist/astro/index.cjs +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/{chunk-OQGEL4TV.js → chunk-3QGWCYTL.js} +1 -1
- package/dist/{chunk-W6AVX6FI.js → chunk-PFJO4TYL.js} +52 -5
- package/dist/{chunk-Y7DWVOOY.js → chunk-U7TRFVK7.js} +1 -1
- package/dist/{chunk-NSQOGI5K.js → chunk-VQIPTUN4.js} +1 -1
- package/dist/graphql/codegen.cjs +51 -4
- package/dist/graphql/codegen.js +3 -3
- package/dist/graphql.cjs +89 -10
- package/dist/graphql.d.cts +69 -39
- package/dist/graphql.d.ts +69 -39
- package/dist/graphql.js +38 -9
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/nextjs/{chunk-PIQF5ZI5.js → chunk-CZK7SMBD.js} +58 -53
- package/dist/nextjs/components.cjs +175 -242
- package/dist/nextjs/components.d.cts +25 -21
- package/dist/nextjs/components.d.ts +25 -21
- package/dist/nextjs/components.js +167 -236
- package/dist/nextjs/graphql.cjs +372 -509
- package/dist/nextjs/graphql.d.cts +131 -135
- package/dist/nextjs/graphql.d.ts +131 -135
- package/dist/nextjs/graphql.js +307 -456
- package/dist/nextjs/index.cjs +114 -54
- package/dist/nextjs/index.d.cts +57 -33
- package/dist/nextjs/index.d.ts +57 -33
- package/dist/nextjs/index.js +108 -5
- package/dist/nuxt/{chunk-PF6GVWHV.js → chunk-JL33ENCM.js} +1 -1
- package/dist/nuxt/graphql.cjs +58 -10
- package/dist/nuxt/graphql.d.cts +33 -38
- package/dist/nuxt/graphql.d.ts +33 -38
- package/dist/nuxt/graphql.js +59 -11
- package/dist/nuxt/index.cjs +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/react/components.cjs +1 -1
- package/dist/react/components.js +1 -1
- package/dist/sveltekit/{chunk-5K7HWMIF.js → chunk-S7S276V7.js} +1 -1
- package/dist/sveltekit/graphql.cjs +58 -10
- package/dist/sveltekit/graphql.d.cts +33 -38
- package/dist/sveltekit/graphql.d.ts +33 -38
- package/dist/sveltekit/graphql.js +59 -11
- package/dist/sveltekit/index.cjs +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/package.json +1 -1
package/dist/nextjs/index.cjs
CHANGED
|
@@ -20,13 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/nextjs/index.ts
|
|
21
21
|
var nextjs_exports = {};
|
|
22
22
|
__export(nextjs_exports, {
|
|
23
|
-
PREPR_BUCKET_COOKIE: () => PREPR_BUCKET_COOKIE,
|
|
24
23
|
PreprError: () => PreprError,
|
|
25
24
|
PreprPersonalizationSwitch: () => import_components.PreprPersonalizationSwitch,
|
|
26
25
|
PreprToolbar: () => import_components.PreprToolbar,
|
|
27
26
|
PreprTrackingPixel: () => import_components.PreprTrackingPixel,
|
|
28
27
|
createPreprDraftRoute: () => createPreprDraftRoute,
|
|
29
28
|
createPreprMiddleware: () => createPreprMiddleware,
|
|
29
|
+
createPreprWebhookRoute: () => createPreprWebhookRoute,
|
|
30
30
|
extractAccessToken: () => extractAccessToken,
|
|
31
31
|
getActiveSegment: () => getActiveSegment,
|
|
32
32
|
getActiveVariant: () => getActiveVariant,
|
|
@@ -35,7 +35,6 @@ __export(nextjs_exports, {
|
|
|
35
35
|
getToolbarProps: () => getToolbarProps,
|
|
36
36
|
isPersonalizedResponse: () => isPersonalizedResponse,
|
|
37
37
|
preprDraftRedirect: () => preprDraftRedirect,
|
|
38
|
-
resolvePreprBucket: () => resolvePreprBucket,
|
|
39
38
|
validatePreprToken: () => validatePreprToken
|
|
40
39
|
});
|
|
41
40
|
module.exports = __toCommonJS(nextjs_exports);
|
|
@@ -44,7 +43,7 @@ module.exports = __toCommonJS(nextjs_exports);
|
|
|
44
43
|
var import_server = require("next/server");
|
|
45
44
|
|
|
46
45
|
// src/version.ts
|
|
47
|
-
var VERSION = "0.6.0-alpha.
|
|
46
|
+
var VERSION = "0.6.0-alpha.7";
|
|
48
47
|
|
|
49
48
|
// src/core/constants.ts
|
|
50
49
|
var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
|
|
@@ -470,55 +469,6 @@ function isPersonalizedResponse(source) {
|
|
|
470
469
|
return PERSONALIZED_VARY.test(headers2.get("vary") ?? "");
|
|
471
470
|
}
|
|
472
471
|
|
|
473
|
-
// src/nextjs/bucket.ts
|
|
474
|
-
var PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
|
|
475
|
-
var isBucket = (value) => value === "A" || value === "B";
|
|
476
|
-
function setBucketCookie(request, response, bucket) {
|
|
477
|
-
response.cookies.set(PREPR_BUCKET_COOKIE, bucket, {
|
|
478
|
-
path: "/",
|
|
479
|
-
sameSite: "lax",
|
|
480
|
-
// Readable on purpose: PreprStack's inline switch reads it before first
|
|
481
|
-
// paint. It holds only "A" or "B".
|
|
482
|
-
httpOnly: false,
|
|
483
|
-
secure: request.nextUrl.protocol === "https:",
|
|
484
|
-
maxAge: 60 * 60 * 24 * 365
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
async function resolvePreprBucket(request, response, options) {
|
|
488
|
-
const stored = request.cookies.get(PREPR_BUCKET_COOKIE)?.value;
|
|
489
|
-
const forced = response.headers.get("x-middleware-request-prepr-abtesting") ?? request.cookies.get(COOKIE_VARIANT)?.value;
|
|
490
|
-
if (options.preview && isBucket(forced)) {
|
|
491
|
-
if (forced !== stored) setBucketCookie(request, response, forced);
|
|
492
|
-
return forced;
|
|
493
|
-
}
|
|
494
|
-
if (isBucket(stored)) return stored;
|
|
495
|
-
if (options.onlyIf) {
|
|
496
|
-
const needed = await Promise.resolve().then(options.onlyIf).catch(() => true);
|
|
497
|
-
if (!needed) return "A";
|
|
498
|
-
}
|
|
499
|
-
const customerId = request.cookies.get(COOKIE_UID)?.value ?? response.cookies.get(COOKIE_UID)?.value;
|
|
500
|
-
if (!customerId) return "A";
|
|
501
|
-
try {
|
|
502
|
-
const res = await fetch(options.url, {
|
|
503
|
-
method: "POST",
|
|
504
|
-
headers: {
|
|
505
|
-
"Content-Type": "application/json",
|
|
506
|
-
"Prepr-Customer-Id": customerId
|
|
507
|
-
},
|
|
508
|
-
body: JSON.stringify({ query: "{ __typename }" }),
|
|
509
|
-
cache: "no-store",
|
|
510
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
|
|
511
|
-
});
|
|
512
|
-
const bucket = res.headers.get("x-prepr-customer-bucket");
|
|
513
|
-
if (isBucket(bucket)) {
|
|
514
|
-
setBucketCookie(request, response, bucket);
|
|
515
|
-
return bucket;
|
|
516
|
-
}
|
|
517
|
-
} catch {
|
|
518
|
-
}
|
|
519
|
-
return "A";
|
|
520
|
-
}
|
|
521
|
-
|
|
522
472
|
// src/nextjs/draft.ts
|
|
523
473
|
var import_headers2 = require("next/headers");
|
|
524
474
|
var import_navigation = require("next/navigation");
|
|
@@ -569,17 +519,128 @@ function createPreprDraftRoute(options) {
|
|
|
569
519
|
};
|
|
570
520
|
}
|
|
571
521
|
|
|
522
|
+
// src/nextjs/webhook.ts
|
|
523
|
+
var import_cache = require("next/cache");
|
|
524
|
+
|
|
525
|
+
// src/core/graphql/cache-tags.ts
|
|
526
|
+
var PREPR_ANY_ITEM_TAG = "prepr:any";
|
|
527
|
+
var PREPR_SCHEMA_TAG = "prepr:schema";
|
|
528
|
+
var preprItemTag = (id) => `prepr:${id}`;
|
|
529
|
+
var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
|
|
530
|
+
var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
|
|
531
|
+
|
|
532
|
+
// src/nextjs/webhook.ts
|
|
533
|
+
var CONTENT_EVENTS = /* @__PURE__ */ new Set([
|
|
534
|
+
"content_item.published",
|
|
535
|
+
"content_item.unpublished",
|
|
536
|
+
"content_item.deleted",
|
|
537
|
+
"content_item.invalidated",
|
|
538
|
+
"asset.changed",
|
|
539
|
+
"asset.deleted"
|
|
540
|
+
]);
|
|
541
|
+
var MODEL_EVENTS = /* @__PURE__ */ new Set([
|
|
542
|
+
"content_item.published",
|
|
543
|
+
"content_item.unpublished",
|
|
544
|
+
"content_item.deleted"
|
|
545
|
+
]);
|
|
546
|
+
var MAX_AGE_MS = 5 * 60 * 1e3;
|
|
547
|
+
var HEX_SIGNATURE = /^[0-9a-f]{64}$/i;
|
|
548
|
+
var BASE64_SIGNATURE = /^[A-Za-z0-9+/]{43}=$/;
|
|
549
|
+
var SIGNATURE_PREFIX = /^sha256=/i;
|
|
550
|
+
var MAX_ID_LENGTH = 200;
|
|
551
|
+
var text = (body, status) => new Response(body, { status, headers: { "Content-Type": "text/plain" } });
|
|
552
|
+
function signatureBytes(header) {
|
|
553
|
+
const value = header.trim().replace(SIGNATURE_PREFIX, "");
|
|
554
|
+
if (HEX_SIGNATURE.test(value)) {
|
|
555
|
+
const bytes = new Uint8Array(32);
|
|
556
|
+
for (let i = 0; i < 32; i++) {
|
|
557
|
+
bytes[i] = parseInt(value.slice(i * 2, i * 2 + 2), 16);
|
|
558
|
+
}
|
|
559
|
+
return bytes;
|
|
560
|
+
}
|
|
561
|
+
if (BASE64_SIGNATURE.test(value)) {
|
|
562
|
+
return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
async function verify(secret, signature, body) {
|
|
567
|
+
const key = await crypto.subtle.importKey(
|
|
568
|
+
"raw",
|
|
569
|
+
new TextEncoder().encode(secret),
|
|
570
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
571
|
+
false,
|
|
572
|
+
["verify"]
|
|
573
|
+
);
|
|
574
|
+
return crypto.subtle.verify("HMAC", key, signature, body);
|
|
575
|
+
}
|
|
576
|
+
function parse(body) {
|
|
577
|
+
try {
|
|
578
|
+
const value = JSON.parse(new TextDecoder().decode(body));
|
|
579
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function eventTime(value) {
|
|
585
|
+
if (typeof value === "number") return value * 1e3;
|
|
586
|
+
if (typeof value === "string") return Date.parse(value);
|
|
587
|
+
return NaN;
|
|
588
|
+
}
|
|
589
|
+
function field(value, key) {
|
|
590
|
+
return typeof value === "object" && value !== null ? value[key] : void 0;
|
|
591
|
+
}
|
|
592
|
+
var usable = (value) => typeof value === "string" && value !== "" && value.length <= MAX_ID_LENGTH;
|
|
593
|
+
function tagsFor(event) {
|
|
594
|
+
const name = event.event;
|
|
595
|
+
if (name === "schema.changed") return [PREPR_SCHEMA_TAG];
|
|
596
|
+
if (typeof name !== "string" || !CONTENT_EVENTS.has(name)) return [];
|
|
597
|
+
const tags = [PREPR_ANY_ITEM_TAG];
|
|
598
|
+
const id = field(event.payload, "id");
|
|
599
|
+
if (usable(id)) tags.push(preprItemTag(id));
|
|
600
|
+
const model = field(field(event.payload, "model"), "body_singular");
|
|
601
|
+
if (MODEL_EVENTS.has(name) && usable(model)) tags.push(preprModelTag(model));
|
|
602
|
+
return tags;
|
|
603
|
+
}
|
|
604
|
+
function createPreprWebhookRoute(options) {
|
|
605
|
+
const { profile = { expire: 0 } } = options;
|
|
606
|
+
return async (request) => {
|
|
607
|
+
const { secret } = options;
|
|
608
|
+
if (!secret) return text("Webhook secret not configured", 500);
|
|
609
|
+
const signature = signatureBytes(
|
|
610
|
+
request.headers.get("prepr-signature") ?? ""
|
|
611
|
+
);
|
|
612
|
+
let body;
|
|
613
|
+
try {
|
|
614
|
+
body = await request.arrayBuffer();
|
|
615
|
+
} catch {
|
|
616
|
+
return text("Unreadable body", 400);
|
|
617
|
+
}
|
|
618
|
+
if (!signature || !await verify(secret, signature, body)) {
|
|
619
|
+
return text("Invalid signature", 401);
|
|
620
|
+
}
|
|
621
|
+
const event = parse(body);
|
|
622
|
+
if (!event) return text("Invalid body", 400);
|
|
623
|
+
const sentAt = eventTime(event.created_on);
|
|
624
|
+
if (Number.isFinite(sentAt) && Math.abs(Date.now() - sentAt) > MAX_AGE_MS) {
|
|
625
|
+
return text("Expired", 401);
|
|
626
|
+
}
|
|
627
|
+
const tags = tagsFor(event);
|
|
628
|
+
for (const tag of tags) (0, import_cache.revalidateTag)(tag, profile);
|
|
629
|
+
return Response.json({ revalidated: tags });
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
572
633
|
// src/nextjs/index.ts
|
|
573
634
|
var import_components = require("./components.cjs");
|
|
574
635
|
// Annotate the CommonJS export names for ESM import in node:
|
|
575
636
|
0 && (module.exports = {
|
|
576
|
-
PREPR_BUCKET_COOKIE,
|
|
577
637
|
PreprError,
|
|
578
638
|
PreprPersonalizationSwitch,
|
|
579
639
|
PreprToolbar,
|
|
580
640
|
PreprTrackingPixel,
|
|
581
641
|
createPreprDraftRoute,
|
|
582
642
|
createPreprMiddleware,
|
|
643
|
+
createPreprWebhookRoute,
|
|
583
644
|
extractAccessToken,
|
|
584
645
|
getActiveSegment,
|
|
585
646
|
getActiveVariant,
|
|
@@ -588,6 +649,5 @@ var import_components = require("./components.cjs");
|
|
|
588
649
|
getToolbarProps,
|
|
589
650
|
isPersonalizedResponse,
|
|
590
651
|
preprDraftRedirect,
|
|
591
|
-
resolvePreprBucket,
|
|
592
652
|
validatePreprToken
|
|
593
653
|
});
|
package/dist/nextjs/index.d.cts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { a as PreprFeatures, P as PreprHeaders, c as PreprToolbarProps, d as PreprToolbarComponentProps } from './server-BL-sqZvu.cjs';
|
|
3
3
|
export { b as PreprError, e as extractAccessToken, i as isPersonalizedResponse, v as validatePreprToken } from './server-BL-sqZvu.cjs';
|
|
4
|
-
import { ReactElement } from 'react';
|
|
5
4
|
|
|
6
5
|
interface PreprMiddlewareOptions {
|
|
7
6
|
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
@@ -45,26 +44,6 @@ declare function getPreprHeaders(): Promise<PreprHeaders>;
|
|
|
45
44
|
*/
|
|
46
45
|
declare function getToolbarProps(token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
|
|
47
46
|
|
|
48
|
-
/** Cookie holding the visitor's A/B bucket, as Prepr assigned it. */
|
|
49
|
-
declare const PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
|
|
50
|
-
type PreprBucket = 'A' | 'B';
|
|
51
|
-
interface ResolvePreprBucketOptions {
|
|
52
|
-
/** Full Prepr GraphQL URL. */
|
|
53
|
-
url: string;
|
|
54
|
-
/** Preview mode: the toolbar's A/B override wins. */
|
|
55
|
-
preview?: boolean;
|
|
56
|
-
/** Abort the one-time lookup after this many ms. Default 1500. */
|
|
57
|
-
timeoutMs?: number;
|
|
58
|
-
/**
|
|
59
|
-
* Look the bucket up only when this resolves to true, typically "does this
|
|
60
|
-
* page have an A/B test": `() => prepr.hasAbTests(PageDocument, { slug })`.
|
|
61
|
-
* Only called for visitors without a bucket cookie, so visitors on pages
|
|
62
|
-
* without A/B tests never wait for the lookup.
|
|
63
|
-
*/
|
|
64
|
-
onlyIf?: () => boolean | Promise<boolean>;
|
|
65
|
-
}
|
|
66
|
-
declare function resolvePreprBucket(request: NextRequest, response: NextResponse, options: ResolvePreprBucketOptions): Promise<PreprBucket>;
|
|
67
|
-
|
|
68
47
|
interface PreprDraftOptions {
|
|
69
48
|
/** Whether this deployment serves preview content. Outside it, both helpers do nothing. */
|
|
70
49
|
preview: boolean;
|
|
@@ -101,6 +80,47 @@ declare function preprDraftRedirect(request: NextRequest, options: PreprDraftOpt
|
|
|
101
80
|
*/
|
|
102
81
|
declare function createPreprDraftRoute(options: Pick<PreprDraftOptions, 'preview'>): (request: Request) => Promise<Response>;
|
|
103
82
|
|
|
83
|
+
/** How a revalidated page refreshes, as Next.js's `revalidateTag` takes it. */
|
|
84
|
+
type PreprRevalidateProfile = string | {
|
|
85
|
+
expire?: number;
|
|
86
|
+
};
|
|
87
|
+
interface PreprWebhookOptions {
|
|
88
|
+
/**
|
|
89
|
+
* The webhook's ID, shown in Prepr under Settings → Webhooks. Prepr signs
|
|
90
|
+
* each request with it (HMAC-SHA256 of the body, in the `Prepr-Signature`
|
|
91
|
+
* header). Keep it in an environment variable.
|
|
92
|
+
*/
|
|
93
|
+
secret: string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Passed to `revalidateTag`. The default `{ expire: 0 }` shows a publish
|
|
96
|
+
* on the very next request; `'max'` serves the old page once more while
|
|
97
|
+
* the new one renders in the background.
|
|
98
|
+
*/
|
|
99
|
+
profile?: PreprRevalidateProfile;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Route handler for a Prepr webhook that refreshes only the cached pages
|
|
103
|
+
* showing the changed item: those tagged by `query(..., { tagItems: true })`
|
|
104
|
+
* or `preprCacheTags`. On publish, unpublish and delete it also revalidates
|
|
105
|
+
* the item's `preprModelTag`, so list pages and lookups that found nothing
|
|
106
|
+
* pick up new and removed items. `schema.changed` refreshes the schema the
|
|
107
|
+
* client rewrites queries with.
|
|
108
|
+
*
|
|
109
|
+
* Other events (draft edits, new assets, visitors, segments) are
|
|
110
|
+
* acknowledged and revalidate nothing, so the webhook can subscribe to
|
|
111
|
+
* everything.
|
|
112
|
+
*
|
|
113
|
+
* Requests without a valid `Prepr-Signature` or older than five minutes get
|
|
114
|
+
* 401; without `secret` every request gets 500.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // app/api/prepr/webhook/route.ts
|
|
118
|
+
* export const POST = createPreprWebhookRoute({
|
|
119
|
+
* secret: process.env.PREPR_WEBHOOK_SECRET,
|
|
120
|
+
* });
|
|
121
|
+
*/
|
|
122
|
+
declare function createPreprWebhookRoute(options: PreprWebhookOptions): (request: Request) => Promise<Response>;
|
|
123
|
+
|
|
104
124
|
/**
|
|
105
125
|
* Loader + typed facade for Prepr's CDN tracking pixel, so consumers don't have
|
|
106
126
|
* to paste a `<script>` tag. The pixel itself lives on the CDN; this only sets
|
|
@@ -154,16 +174,20 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
154
174
|
* `createPreprPreview` mounts imperatively.
|
|
155
175
|
*/
|
|
156
176
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Decides every client-side navigation to another page: when the visitor
|
|
179
|
+
* presses a same-site link, it asks `createPreprProxy` which variants they
|
|
180
|
+
* get on that page, and holds the click until the answer is in. The new page
|
|
181
|
+
* then paints with those variants, never with the default variant first.
|
|
182
|
+
* Pressing starts the request, so the wait mostly overlaps the click itself.
|
|
183
|
+
*
|
|
184
|
+
* Mount it once in the root layout, so links on pages without a
|
|
185
|
+
* `PreprStack` are covered too. Clicks with a modifier key, links to other
|
|
186
|
+
* sites and links that open a new tab or download are left alone.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* <PreprPersonalizationSwitch />
|
|
190
|
+
*/
|
|
191
|
+
declare function PreprPersonalizationSwitch(): null;
|
|
168
192
|
|
|
169
|
-
export {
|
|
193
|
+
export { type PreprDraftOptions, type PreprMiddlewareOptions, PreprPersonalizationSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, type PreprWebhookOptions, createPreprDraftRoute, createPreprMiddleware, createPreprWebhookRoute, getActiveSegment, getActiveVariant, getPreprHeaders, getPreprUUID, getToolbarProps, preprDraftRedirect };
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { a as PreprFeatures, P as PreprHeaders, c as PreprToolbarProps, d as PreprToolbarComponentProps } from './server-BL-sqZvu.js';
|
|
3
3
|
export { b as PreprError, e as extractAccessToken, i as isPersonalizedResponse, v as validatePreprToken } from './server-BL-sqZvu.js';
|
|
4
|
-
import { ReactElement } from 'react';
|
|
5
4
|
|
|
6
5
|
interface PreprMiddlewareOptions {
|
|
7
6
|
/** Enable preview-mode headers (segment/AB overrides, preview bar). */
|
|
@@ -45,26 +44,6 @@ declare function getPreprHeaders(): Promise<PreprHeaders>;
|
|
|
45
44
|
*/
|
|
46
45
|
declare function getToolbarProps(token: string, features?: PreprFeatures): Promise<PreprToolbarProps>;
|
|
47
46
|
|
|
48
|
-
/** Cookie holding the visitor's A/B bucket, as Prepr assigned it. */
|
|
49
|
-
declare const PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
|
|
50
|
-
type PreprBucket = 'A' | 'B';
|
|
51
|
-
interface ResolvePreprBucketOptions {
|
|
52
|
-
/** Full Prepr GraphQL URL. */
|
|
53
|
-
url: string;
|
|
54
|
-
/** Preview mode: the toolbar's A/B override wins. */
|
|
55
|
-
preview?: boolean;
|
|
56
|
-
/** Abort the one-time lookup after this many ms. Default 1500. */
|
|
57
|
-
timeoutMs?: number;
|
|
58
|
-
/**
|
|
59
|
-
* Look the bucket up only when this resolves to true, typically "does this
|
|
60
|
-
* page have an A/B test": `() => prepr.hasAbTests(PageDocument, { slug })`.
|
|
61
|
-
* Only called for visitors without a bucket cookie, so visitors on pages
|
|
62
|
-
* without A/B tests never wait for the lookup.
|
|
63
|
-
*/
|
|
64
|
-
onlyIf?: () => boolean | Promise<boolean>;
|
|
65
|
-
}
|
|
66
|
-
declare function resolvePreprBucket(request: NextRequest, response: NextResponse, options: ResolvePreprBucketOptions): Promise<PreprBucket>;
|
|
67
|
-
|
|
68
47
|
interface PreprDraftOptions {
|
|
69
48
|
/** Whether this deployment serves preview content. Outside it, both helpers do nothing. */
|
|
70
49
|
preview: boolean;
|
|
@@ -101,6 +80,47 @@ declare function preprDraftRedirect(request: NextRequest, options: PreprDraftOpt
|
|
|
101
80
|
*/
|
|
102
81
|
declare function createPreprDraftRoute(options: Pick<PreprDraftOptions, 'preview'>): (request: Request) => Promise<Response>;
|
|
103
82
|
|
|
83
|
+
/** How a revalidated page refreshes, as Next.js's `revalidateTag` takes it. */
|
|
84
|
+
type PreprRevalidateProfile = string | {
|
|
85
|
+
expire?: number;
|
|
86
|
+
};
|
|
87
|
+
interface PreprWebhookOptions {
|
|
88
|
+
/**
|
|
89
|
+
* The webhook's ID, shown in Prepr under Settings → Webhooks. Prepr signs
|
|
90
|
+
* each request with it (HMAC-SHA256 of the body, in the `Prepr-Signature`
|
|
91
|
+
* header). Keep it in an environment variable.
|
|
92
|
+
*/
|
|
93
|
+
secret: string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Passed to `revalidateTag`. The default `{ expire: 0 }` shows a publish
|
|
96
|
+
* on the very next request; `'max'` serves the old page once more while
|
|
97
|
+
* the new one renders in the background.
|
|
98
|
+
*/
|
|
99
|
+
profile?: PreprRevalidateProfile;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Route handler for a Prepr webhook that refreshes only the cached pages
|
|
103
|
+
* showing the changed item: those tagged by `query(..., { tagItems: true })`
|
|
104
|
+
* or `preprCacheTags`. On publish, unpublish and delete it also revalidates
|
|
105
|
+
* the item's `preprModelTag`, so list pages and lookups that found nothing
|
|
106
|
+
* pick up new and removed items. `schema.changed` refreshes the schema the
|
|
107
|
+
* client rewrites queries with.
|
|
108
|
+
*
|
|
109
|
+
* Other events (draft edits, new assets, visitors, segments) are
|
|
110
|
+
* acknowledged and revalidate nothing, so the webhook can subscribe to
|
|
111
|
+
* everything.
|
|
112
|
+
*
|
|
113
|
+
* Requests without a valid `Prepr-Signature` or older than five minutes get
|
|
114
|
+
* 401; without `secret` every request gets 500.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // app/api/prepr/webhook/route.ts
|
|
118
|
+
* export const POST = createPreprWebhookRoute({
|
|
119
|
+
* secret: process.env.PREPR_WEBHOOK_SECRET,
|
|
120
|
+
* });
|
|
121
|
+
*/
|
|
122
|
+
declare function createPreprWebhookRoute(options: PreprWebhookOptions): (request: Request) => Promise<Response>;
|
|
123
|
+
|
|
104
124
|
/**
|
|
105
125
|
* Loader + typed facade for Prepr's CDN tracking pixel, so consumers don't have
|
|
106
126
|
* to paste a `<script>` tag. The pixel itself lives on the CDN; this only sets
|
|
@@ -154,16 +174,20 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
154
174
|
* `createPreprPreview` mounts imperatively.
|
|
155
175
|
*/
|
|
156
176
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Decides every client-side navigation to another page: when the visitor
|
|
179
|
+
* presses a same-site link, it asks `createPreprProxy` which variants they
|
|
180
|
+
* get on that page, and holds the click until the answer is in. The new page
|
|
181
|
+
* then paints with those variants, never with the default variant first.
|
|
182
|
+
* Pressing starts the request, so the wait mostly overlaps the click itself.
|
|
183
|
+
*
|
|
184
|
+
* Mount it once in the root layout, so links on pages without a
|
|
185
|
+
* `PreprStack` are covered too. Clicks with a modifier key, links to other
|
|
186
|
+
* sites and links that open a new tab or download are left alone.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* <PreprPersonalizationSwitch />
|
|
190
|
+
*/
|
|
191
|
+
declare function PreprPersonalizationSwitch(): null;
|
|
168
192
|
|
|
169
|
-
export {
|
|
193
|
+
export { type PreprDraftOptions, type PreprMiddlewareOptions, PreprPersonalizationSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, type PreprWebhookOptions, createPreprDraftRoute, createPreprMiddleware, createPreprWebhookRoute, getActiveSegment, getActiveVariant, getPreprHeaders, getPreprUUID, getToolbarProps, preprDraftRedirect };
|
package/dist/nextjs/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
PREPR_ANY_ITEM_TAG,
|
|
3
|
+
PREPR_SCHEMA_TAG,
|
|
3
4
|
PreprError,
|
|
4
5
|
createPreprDraftRoute,
|
|
5
6
|
createPreprMiddleware,
|
|
@@ -11,9 +12,112 @@ import {
|
|
|
11
12
|
getToolbarProps,
|
|
12
13
|
isPersonalizedResponse,
|
|
13
14
|
preprDraftRedirect,
|
|
14
|
-
|
|
15
|
+
preprItemTag,
|
|
16
|
+
preprModelTag,
|
|
15
17
|
validatePreprToken
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CZK7SMBD.js";
|
|
19
|
+
|
|
20
|
+
// src/nextjs/webhook.ts
|
|
21
|
+
import { revalidateTag } from "next/cache";
|
|
22
|
+
var CONTENT_EVENTS = /* @__PURE__ */ new Set([
|
|
23
|
+
"content_item.published",
|
|
24
|
+
"content_item.unpublished",
|
|
25
|
+
"content_item.deleted",
|
|
26
|
+
"content_item.invalidated",
|
|
27
|
+
"asset.changed",
|
|
28
|
+
"asset.deleted"
|
|
29
|
+
]);
|
|
30
|
+
var MODEL_EVENTS = /* @__PURE__ */ new Set([
|
|
31
|
+
"content_item.published",
|
|
32
|
+
"content_item.unpublished",
|
|
33
|
+
"content_item.deleted"
|
|
34
|
+
]);
|
|
35
|
+
var MAX_AGE_MS = 5 * 60 * 1e3;
|
|
36
|
+
var HEX_SIGNATURE = /^[0-9a-f]{64}$/i;
|
|
37
|
+
var BASE64_SIGNATURE = /^[A-Za-z0-9+/]{43}=$/;
|
|
38
|
+
var SIGNATURE_PREFIX = /^sha256=/i;
|
|
39
|
+
var MAX_ID_LENGTH = 200;
|
|
40
|
+
var text = (body, status) => new Response(body, { status, headers: { "Content-Type": "text/plain" } });
|
|
41
|
+
function signatureBytes(header) {
|
|
42
|
+
const value = header.trim().replace(SIGNATURE_PREFIX, "");
|
|
43
|
+
if (HEX_SIGNATURE.test(value)) {
|
|
44
|
+
const bytes = new Uint8Array(32);
|
|
45
|
+
for (let i = 0; i < 32; i++) {
|
|
46
|
+
bytes[i] = parseInt(value.slice(i * 2, i * 2 + 2), 16);
|
|
47
|
+
}
|
|
48
|
+
return bytes;
|
|
49
|
+
}
|
|
50
|
+
if (BASE64_SIGNATURE.test(value)) {
|
|
51
|
+
return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
async function verify(secret, signature, body) {
|
|
56
|
+
const key = await crypto.subtle.importKey(
|
|
57
|
+
"raw",
|
|
58
|
+
new TextEncoder().encode(secret),
|
|
59
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
60
|
+
false,
|
|
61
|
+
["verify"]
|
|
62
|
+
);
|
|
63
|
+
return crypto.subtle.verify("HMAC", key, signature, body);
|
|
64
|
+
}
|
|
65
|
+
function parse(body) {
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(new TextDecoder().decode(body));
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function eventTime(value) {
|
|
74
|
+
if (typeof value === "number") return value * 1e3;
|
|
75
|
+
if (typeof value === "string") return Date.parse(value);
|
|
76
|
+
return NaN;
|
|
77
|
+
}
|
|
78
|
+
function field(value, key) {
|
|
79
|
+
return typeof value === "object" && value !== null ? value[key] : void 0;
|
|
80
|
+
}
|
|
81
|
+
var usable = (value) => typeof value === "string" && value !== "" && value.length <= MAX_ID_LENGTH;
|
|
82
|
+
function tagsFor(event) {
|
|
83
|
+
const name = event.event;
|
|
84
|
+
if (name === "schema.changed") return [PREPR_SCHEMA_TAG];
|
|
85
|
+
if (typeof name !== "string" || !CONTENT_EVENTS.has(name)) return [];
|
|
86
|
+
const tags = [PREPR_ANY_ITEM_TAG];
|
|
87
|
+
const id = field(event.payload, "id");
|
|
88
|
+
if (usable(id)) tags.push(preprItemTag(id));
|
|
89
|
+
const model = field(field(event.payload, "model"), "body_singular");
|
|
90
|
+
if (MODEL_EVENTS.has(name) && usable(model)) tags.push(preprModelTag(model));
|
|
91
|
+
return tags;
|
|
92
|
+
}
|
|
93
|
+
function createPreprWebhookRoute(options) {
|
|
94
|
+
const { profile = { expire: 0 } } = options;
|
|
95
|
+
return async (request) => {
|
|
96
|
+
const { secret } = options;
|
|
97
|
+
if (!secret) return text("Webhook secret not configured", 500);
|
|
98
|
+
const signature = signatureBytes(
|
|
99
|
+
request.headers.get("prepr-signature") ?? ""
|
|
100
|
+
);
|
|
101
|
+
let body;
|
|
102
|
+
try {
|
|
103
|
+
body = await request.arrayBuffer();
|
|
104
|
+
} catch {
|
|
105
|
+
return text("Unreadable body", 400);
|
|
106
|
+
}
|
|
107
|
+
if (!signature || !await verify(secret, signature, body)) {
|
|
108
|
+
return text("Invalid signature", 401);
|
|
109
|
+
}
|
|
110
|
+
const event = parse(body);
|
|
111
|
+
if (!event) return text("Invalid body", 400);
|
|
112
|
+
const sentAt = eventTime(event.created_on);
|
|
113
|
+
if (Number.isFinite(sentAt) && Math.abs(Date.now() - sentAt) > MAX_AGE_MS) {
|
|
114
|
+
return text("Expired", 401);
|
|
115
|
+
}
|
|
116
|
+
const tags = tagsFor(event);
|
|
117
|
+
for (const tag of tags) revalidateTag(tag, profile);
|
|
118
|
+
return Response.json({ revalidated: tags });
|
|
119
|
+
};
|
|
120
|
+
}
|
|
17
121
|
|
|
18
122
|
// src/nextjs/index.ts
|
|
19
123
|
import {
|
|
@@ -22,13 +126,13 @@ import {
|
|
|
22
126
|
PreprTrackingPixel
|
|
23
127
|
} from "./components.js";
|
|
24
128
|
export {
|
|
25
|
-
PREPR_BUCKET_COOKIE,
|
|
26
129
|
PreprError,
|
|
27
130
|
PreprPersonalizationSwitch,
|
|
28
131
|
PreprToolbar,
|
|
29
132
|
PreprTrackingPixel,
|
|
30
133
|
createPreprDraftRoute,
|
|
31
134
|
createPreprMiddleware,
|
|
135
|
+
createPreprWebhookRoute,
|
|
32
136
|
extractAccessToken,
|
|
33
137
|
getActiveSegment,
|
|
34
138
|
getActiveVariant,
|
|
@@ -37,6 +141,5 @@ export {
|
|
|
37
141
|
getToolbarProps,
|
|
38
142
|
isPersonalizedResponse,
|
|
39
143
|
preprDraftRedirect,
|
|
40
|
-
resolvePreprBucket,
|
|
41
144
|
validatePreprToken
|
|
42
145
|
};
|