@replohq/sdk 0.1.0 → 0.2.1

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.
@@ -0,0 +1,43 @@
1
+ export declare const getValidFilename: (value: string) => string;
2
+ export declare function slugify(text: string): string;
3
+ export declare function metafieldContainsHtml(metafieldValue: string): boolean;
4
+ export declare function capitalizeFirstLetter(inputString: string): string;
5
+ /**
6
+ * Get url without query params.
7
+ *
8
+ * @author Kurt 2024-10-14
9
+ */
10
+ export declare const getUrlWithoutQueryParams: (url: string) => string | undefined;
11
+ /**
12
+ * Converts PascalCase strings to readable format with spaces.
13
+ * E.g., "LandingPage1" -> "Landing Page 1"
14
+ *
15
+ * @param pascalCaseString - The PascalCase string to convert
16
+ * @returns A readable string with spaces between words and numbers
17
+ */
18
+ export declare const convertPascalCaseToReadable: (pascalCaseString: string) => string;
19
+ export declare function formatWithCatN({ content, startLineNumber, }: {
20
+ content: string;
21
+ startLineNumber?: number;
22
+ }): string;
23
+ /**
24
+ * Escapes special regex characters in a string for use in RegExp constructor
25
+ */
26
+ export declare function escapeRegExp(string: string): string;
27
+ /**
28
+ * Skill names are authored as kebab-case slugs in SKILL.md frontmatter
29
+ * ("site-management", "founder-review"). Anywhere we render the name to the
30
+ * user — skills app cards, the chat tool feed, the provisioning overlay — we
31
+ * run it through this so they see "Site Management", "Founder Review", etc.
32
+ * instead of the raw slug.
33
+ *
34
+ * Names that already look human-authored (any whitespace or any uppercase
35
+ * letter) are returned untouched, so a skill can opt out by writing
36
+ * `name: My Custom Skill` in frontmatter.
37
+ */
38
+ export declare function prettifySkillName(name: string): string;
39
+ /**
40
+ * Safely decodes a URI component, returning the original value if decoding fails.
41
+ * Returns null if the input is null or undefined, preserving empty strings.
42
+ */
43
+ export declare function decodeURIComponentSafe(value: string | null | undefined): string | null;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Builds a URLSearchParams from a record, omitting undefined values. Array
3
+ * values are appended as repeated keys; scalars are stringified.
4
+ */
5
+ export declare function buildUrlSearchParams(values: Record<string, string | number | boolean | string[] | undefined>): URLSearchParams;
6
+ export declare function isValidHttpUrl(url: string, options?: {
7
+ allowBlockedHostnames?: boolean;
8
+ allowedHostnames?: string[];
9
+ }): boolean;
10
+ /**
11
+ * Tries to parse and validate the given string as a URL, including protocol-relative
12
+ * URLs. Returns a URL if parsing is possible, otherwise null.
13
+ *
14
+ * Note: Javascript URLs don't recognize protocol-relative URLs (e.g. //google.com)
15
+ * as valid, so we need special casing here. If protocol-relative, the returned
16
+ * url will have an https protocol. This default is arbitrary but backed up by best
17
+ * practice: https://webhint.io/docs/user-guide/hints/hint-no-protocol-relative-urls/
18
+ */
19
+ export declare function normalizeUrlIncludingProtocolRelative(url: string): URL | null;
20
+ export declare function normalizeCustomDomain(input: string): string;
21
+ /**
22
+ * Returns true if `origin` is a valid Replo Vercel preview deployment origin.
23
+ * Matches `https://<slug>.preview.replointernal.com`, e.g.
24
+ *
25
+ * Use this when you have a full origin string (e.g. from `request.headers.origin`).
26
+ */
27
+ export declare function isPreviewOrigin(origin: string): boolean;
28
+ /**
29
+ * Returns true if `hostname` belongs to a Replo Vercel preview deployment.
30
+ * Matches `<slug>.preview.replointernal.com`, e.g.
31
+ *
32
+ * Use this when you've already parsed the URL and have a bare hostname.
33
+ */
34
+ export declare function isPreviewInternalHostname(hostname: string): boolean;
35
+ export declare function normalizeUrlScheme(url: string): string;
36
+ /**
37
+ * Truncates a domain name by shortening the subdomain portion while preserving
38
+ * the main domain suffix. Useful for displaying long domain names in UI.
39
+ *
40
+ * @param domain - The domain to truncate (e.g., "very-long-subdomain.example.com")
41
+ * @param maxLength - Maximum length of the truncated domain (default: 40)
42
+ * @returns Truncated domain with ellipsis (e.g., "very-long-sub...example.com")
43
+ */
44
+ export declare function truncateDomain(domain: string, maxLength?: number): string;
45
+ /**
46
+ * Validates that a route path is properly formatted and URL-safe.
47
+ * Routes must start with / and contain only valid slug segments.
48
+ *
49
+ * @param path - The route path to validate
50
+ * @returns true if the path is a valid route, false otherwise
51
+ */
52
+ export declare const isValidRoutePath: (path: string) => boolean;
53
+ /**
54
+ * Parses a URL, returning null instead of throwing. Pass `base` to resolve a
55
+ * relative value such as a site path.
56
+ */
57
+ export declare function parseUrl(value: string, base?: string): URL | null;
@@ -1,6 +1,6 @@
1
+ import { ReploError } from '../../chunk-5M7VU32I.mjs';
1
2
  import { __commonJS, __toESM } from '../../chunk-UJCSKKID.mjs';
2
3
  import { z } from 'zod';
3
- import { v4 } from 'uuid';
4
4
 
5
5
  // ../../node_modules/.pnpm/first-match@0.0.1/node_modules/first-match/index.js
6
6
  var require_first_match = __commonJS({
@@ -1859,117 +1859,6 @@ var require_currency_codes = __commonJS({
1859
1859
 
1860
1860
  // ../schemas/money.ts
1861
1861
  var import_currency_codes = __toESM(require_currency_codes());
1862
- function randomUUID() {
1863
- return v4();
1864
- }
1865
- var ReploError = class _ReploError extends Error {
1866
- config;
1867
- id;
1868
- constructor(config) {
1869
- super(config.message);
1870
- this.message = config.message ?? this.constructor.name;
1871
- this.config = config;
1872
- this.id = randomUUID();
1873
- const cause = config.cause;
1874
- if (cause) {
1875
- this.cause = cause;
1876
- if (cause instanceof _ReploError) {
1877
- this.config.additionalData = {
1878
- ...this.config.additionalData,
1879
- ...cause.config.additionalData
1880
- };
1881
- }
1882
- }
1883
- }
1884
- };
1885
- var errorUserFacingDetailsMessageSchema = z.object({
1886
- type: z.enum([
1887
- "toast",
1888
- "fullPageModal",
1889
- "shopifyStorePassword",
1890
- "billing.addPublishedElements",
1891
- "billing.addShopifyThemeProject",
1892
- "billing.addReploSite",
1893
- // Agent Billing ONLY
1894
- "agentBilling.shopifyThemeBuilder.publishedElements.exceeded",
1895
- "agentBilling.shopifyThemeBuilder.projects.exceeded",
1896
- "agentBilling.sites.exceeded",
1897
- "agentBilling.credits.exceeded",
1898
- "agentBilling.upgradeRequired"
1899
- ]),
1900
- message: z.string(),
1901
- isRecoverable: z.boolean().optional(),
1902
- // NOTE (Matt 2024-03-22): We support detail being an array of strings
1903
- // in FullPageModal only.
1904
- detail: z.array(z.string()).or(z.string()).optional(),
1905
- key: z.string(),
1906
- usage: z.object({
1907
- current: z.number(),
1908
- maximum: z.number()
1909
- }).optional(),
1910
- plan: z.string().optional(),
1911
- callToAction: z.discriminatedUnion("type", [
1912
- z.object({ type: z.literal("reload") }),
1913
- z.object({ type: z.literal("installApp") }),
1914
- z.object({ type: z.literal("goToHomepage") }),
1915
- z.object({ type: z.literal("navigateToProjectEditor") }),
1916
- z.object({ type: z.literal("closeModal") }),
1917
- z.object({
1918
- type: z.literal("navigateToLink"),
1919
- link: z.discriminatedUnion("type", [
1920
- z.object({ type: z.literal("errors.wrongJsonExtension") }),
1921
- z.object({ type: z.literal("errors.chunkingError") }),
1922
- z.object({ type: z.literal("errors.indexBackupError") }),
1923
- z.object({
1924
- type: z.literal("errors.shopifyIntegrationAlreadyAssigned")
1925
- }),
1926
- z.object({
1927
- type: z.literal("errors.shopifyIntegrationMissingAccessScopes"),
1928
- url: z.string()
1929
- }),
1930
- z.object({
1931
- type: z.literal("customLink"),
1932
- url: z.string()
1933
- }),
1934
- z.object({
1935
- type: z.literal(
1936
- "errors.projectConnectedToBillingIntegrationForShopifyWorkspace"
1937
- )
1938
- }),
1939
- z.object({ type: z.literal("errors.themeAssetSizeLimitExceeded") }),
1940
- z.object({ type: z.literal("errors.themeTotalSizeLimitExceeded") }),
1941
- z.object({
1942
- type: z.literal("errors.pageMetafieldDefinitionsChanged")
1943
- }),
1944
- z.object({ type: z.literal("settings.billing") })
1945
- ])
1946
- })
1947
- ]).optional()
1948
- }).describe("ErrorUserFacingDetailsWithMessage");
1949
- z.discriminatedUnion("type", [
1950
- /**
1951
- * Normal errors, show error UI depending on the type
1952
- */
1953
- errorUserFacingDetailsMessageSchema,
1954
- /**
1955
- * Errors which should be ignored by the frontend. This is uncommon, usually
1956
- * you want to show an error in the UI, and if you don't want to then you catch
1957
- * the error in your route.
1958
- */
1959
- z.object({
1960
- type: z.literal("uncommon_none"),
1961
- key: z.string(),
1962
- usage: z.object({
1963
- current: z.number(),
1964
- maximum: z.number()
1965
- }).optional(),
1966
- message: z.string().optional(),
1967
- isRecoverable: z.boolean().optional(),
1968
- detail: z.array(z.string()).or(z.string()).optional()
1969
- })
1970
- ]).describe("ErrorUserFacingDetails");
1971
-
1972
- // ../schemas/money.ts
1973
1862
  var InvalidAmountError = class extends ReploError {
1974
1863
  };
1975
1864
  var UnsupportedCurrencyError = class extends ReploError {
@@ -0,0 +1,38 @@
1
+ /** Captured segment values, keyed by param name. */
2
+ type RouteParams = Record<string, string>;
3
+ /**
4
+ * The params captured from `pathname`, or null when the source does not match.
5
+ * An empty object means the source matched with nothing to capture.
6
+ */
7
+ export declare function matchSource({ source, pathname, }: {
8
+ source: string;
9
+ pathname: string;
10
+ }): RouteParams | null;
11
+ /**
12
+ * Whether a destination carries any `:param`, anywhere — including inside a
13
+ * query, which splitting on `/` would miss.
14
+ */
15
+ export declare function hasRouteParam(value: string): boolean;
16
+ /**
17
+ * Every `:param` in the string, in order, each written back with its modifier.
18
+ * The modifier is part of the identity: a source's `:page?` may capture
19
+ * nothing, so a destination's `:page` is not the same param.
20
+ */
21
+ export declare function getRouteParams(value: string): string[];
22
+ /**
23
+ * Substitutes captured params into a destination in one pass, so a substituted
24
+ * value is never rescanned as though it were itself a param. A param the
25
+ * request never captured collapses to nothing when it was optional, and is
26
+ * otherwise left in place for the caller to notice.
27
+ */
28
+ export declare function applyParams({ value, params, }: {
29
+ value: string;
30
+ params: RouteParams;
31
+ }): string;
32
+ export interface ParamSegment {
33
+ name: string;
34
+ modifier: "*" | "+" | "?" | null;
35
+ }
36
+ /** Reads `:name`, `:name*`, `:name+`, or `:name?`; null for a literal segment. */
37
+ export declare function getParamSegment(segment: string): ParamSegment | null;
38
+ export {};
@@ -0,0 +1,2 @@
1
+ export { applyParams, getParamSegment, getRouteParams, hasRouteParam, matchSource } from '../../../chunk-VPEVERWE.mjs';
2
+ import '../../../chunk-UJCSKKID.mjs';
@@ -0,0 +1,30 @@
1
+ import { ReploError } from "../errors";
2
+ import { z } from "zod";
3
+ export declare class RedirectRuleValidationError extends ReploError {
4
+ }
5
+ export declare const redirectStatusSchema: z.ZodUnion<readonly [z.ZodLiteral<301>, z.ZodLiteral<302>, z.ZodLiteral<307>, z.ZodLiteral<308>]>;
6
+ export declare const redirectRuleSchema: z.ZodObject<{
7
+ source: z.ZodString;
8
+ destination: z.ZodString;
9
+ status: z.ZodUnion<readonly [z.ZodLiteral<301>, z.ZodLiteral<302>, z.ZodLiteral<307>, z.ZodLiteral<308>]>;
10
+ }, z.core.$strict>;
11
+ export type RedirectStatus = z.infer<typeof redirectStatusSchema>;
12
+ export type RedirectRule = z.infer<typeof redirectRuleSchema>;
13
+ /**
14
+ * Validates a redirect rule, returning it unchanged. Throws
15
+ * `RedirectRuleValidationError` carrying the first failing check's message.
16
+ */
17
+ export declare function reploRedirect(input: RedirectRule): RedirectRule;
18
+ export interface RedirectConflict {
19
+ blockingIndex: number;
20
+ blockedIndex: number;
21
+ }
22
+ /**
23
+ * Rules evaluate first-match-wins, so an earlier rule that already matches a
24
+ * later rule's own source makes the later rule dead. Compared with the later
25
+ * source's route params replaced, since that is a concrete path the earlier rule
26
+ * would see at request time.
27
+ */
28
+ export declare function findRedirectConflicts(rules: readonly {
29
+ source: string;
30
+ }[]): RedirectConflict[];
@@ -0,0 +1,96 @@
1
+ import { ReploError } from '../../../chunk-5M7VU32I.mjs';
2
+ import { getParamSegment, getRouteParams, hasRouteParam, matchSource } from '../../../chunk-VPEVERWE.mjs';
3
+ import '../../../chunk-UJCSKKID.mjs';
4
+ import { z } from 'zod';
5
+
6
+ // ../replo-utils/lib/url.ts
7
+ function parseUrl(value, base) {
8
+ try {
9
+ return new URL(value, base);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ var RedirectRuleValidationError = class extends ReploError {
15
+ };
16
+ var SITE_PATH_ORIGIN = "https://site.invalid";
17
+ var redirectStatusSchema = z.union([
18
+ z.literal(301),
19
+ z.literal(302),
20
+ z.literal(307),
21
+ z.literal(308)
22
+ ]);
23
+ var sourceSchema = z.string().min(1).refine((source) => source.startsWith("/"), {
24
+ message: "must start with /"
25
+ }).refine((source) => source.split("/").every(hasValidWildcardUsage), {
26
+ message: '"*" is only meaningful on a param segment \u2014 write "/blog/:rest*", not "/blog/*"'
27
+ });
28
+ function hasValidWildcardUsage(segment) {
29
+ if (!segment.includes("*")) {
30
+ return true;
31
+ }
32
+ return getParamSegment(segment)?.modifier === "*";
33
+ }
34
+ var destinationSchema = z.string().min(1).refine(
35
+ (destination) => {
36
+ const concrete = replaceRouteParamsWithSentinel(destination);
37
+ if (concrete.startsWith("/")) {
38
+ return parseUrl(concrete, SITE_PATH_ORIGIN)?.origin === SITE_PATH_ORIGIN;
39
+ }
40
+ const protocol = parseUrl(concrete)?.protocol;
41
+ return protocol === "http:" || protocol === "https:";
42
+ },
43
+ {
44
+ message: "must be a path on this site or an absolute HTTP(S) URL; a destination starting with / must stay on this site"
45
+ }
46
+ );
47
+ var redirectRuleSchema = z.object({
48
+ source: sourceSchema,
49
+ destination: destinationSchema,
50
+ status: redirectStatusSchema
51
+ }).strict().superRefine(({ source, destination }, context) => {
52
+ const captured = new Set(getRouteParams(source));
53
+ for (const routeParam of getRouteParams(destination)) {
54
+ if (!captured.has(routeParam)) {
55
+ context.addIssue({
56
+ code: "custom",
57
+ path: ["destination"],
58
+ message: `":${routeParam}" is not captured by this rule's source, so it would be redirected to literally`
59
+ });
60
+ }
61
+ }
62
+ const concrete = replaceRouteParamsWithSentinel(destination);
63
+ if (hasRouteParam(destination) && concrete.startsWith("/") && matchSource({ source, pathname: concrete }) !== null) {
64
+ context.addIssue({
65
+ code: "custom",
66
+ path: ["destination"],
67
+ message: "destination is matched by this rule's own source, so the redirect would loop forever"
68
+ });
69
+ }
70
+ });
71
+ function reploRedirect(input) {
72
+ const parsed = redirectRuleSchema.safeParse(input);
73
+ if (parsed.success) {
74
+ return parsed.data;
75
+ }
76
+ const message = parsed.error.issues[0]?.message ?? "invalid redirect rule";
77
+ throw new RedirectRuleValidationError({ message });
78
+ }
79
+ function findRedirectConflicts(rules) {
80
+ const conflicts = [];
81
+ rules.forEach((blocked, blockedIndex) => {
82
+ const pathname = replaceRouteParamsWithSentinel(blocked.source);
83
+ const blockingIndex = rules.findIndex((blocking, index) => {
84
+ return index < blockedIndex && matchSource({ source: blocking.source, pathname }) !== null;
85
+ });
86
+ if (blockingIndex >= 0) {
87
+ conflicts.push({ blockingIndex, blockedIndex });
88
+ }
89
+ });
90
+ return conflicts;
91
+ }
92
+ function replaceRouteParamsWithSentinel(value) {
93
+ return value.split("/").map((segment) => getParamSegment(segment) ? "x" : segment).join("/");
94
+ }
95
+
96
+ export { RedirectRuleValidationError, findRedirectConflicts, redirectRuleSchema, redirectStatusSchema, reploRedirect };
@@ -0,0 +1,115 @@
1
+ import { v4 } from 'uuid';
2
+ import { z } from 'zod';
3
+
4
+ // ../replo-utils/lib/crypto.ts
5
+ function randomUUID() {
6
+ return v4();
7
+ }
8
+ var ReploError = class _ReploError extends Error {
9
+ config;
10
+ id;
11
+ constructor(config) {
12
+ super(config.message);
13
+ this.message = config.message ?? this.constructor.name;
14
+ this.config = config;
15
+ this.id = randomUUID();
16
+ const cause = config.cause;
17
+ if (cause) {
18
+ this.cause = cause;
19
+ if (cause instanceof _ReploError) {
20
+ this.config.additionalData = {
21
+ ...this.config.additionalData,
22
+ ...cause.config.additionalData
23
+ };
24
+ }
25
+ }
26
+ }
27
+ };
28
+ var errorUserFacingDetailsMessageSchema = z.object({
29
+ type: z.enum([
30
+ "toast",
31
+ "fullPageModal",
32
+ "shopifyStorePassword",
33
+ "billing.addPublishedElements",
34
+ "billing.addShopifyThemeProject",
35
+ "billing.addReploSite",
36
+ // Agent Billing ONLY
37
+ "agentBilling.shopifyThemeBuilder.publishedElements.exceeded",
38
+ "agentBilling.shopifyThemeBuilder.projects.exceeded",
39
+ "agentBilling.sites.exceeded",
40
+ "agentBilling.credits.exceeded",
41
+ "agentBilling.upgradeRequired"
42
+ ]),
43
+ message: z.string(),
44
+ isRecoverable: z.boolean().optional(),
45
+ // NOTE (Matt 2024-03-22): We support detail being an array of strings
46
+ // in FullPageModal only.
47
+ detail: z.array(z.string()).or(z.string()).optional(),
48
+ key: z.string(),
49
+ usage: z.object({
50
+ current: z.number(),
51
+ maximum: z.number()
52
+ }).optional(),
53
+ plan: z.string().optional(),
54
+ callToAction: z.discriminatedUnion("type", [
55
+ z.object({ type: z.literal("reload") }),
56
+ z.object({ type: z.literal("installApp") }),
57
+ z.object({ type: z.literal("goToHomepage") }),
58
+ z.object({ type: z.literal("navigateToProjectEditor") }),
59
+ z.object({ type: z.literal("closeModal") }),
60
+ z.object({
61
+ type: z.literal("navigateToLink"),
62
+ link: z.discriminatedUnion("type", [
63
+ z.object({ type: z.literal("errors.wrongJsonExtension") }),
64
+ z.object({ type: z.literal("errors.chunkingError") }),
65
+ z.object({ type: z.literal("errors.indexBackupError") }),
66
+ z.object({
67
+ type: z.literal("errors.shopifyIntegrationAlreadyAssigned")
68
+ }),
69
+ z.object({
70
+ type: z.literal("errors.shopifyIntegrationMissingAccessScopes"),
71
+ url: z.string()
72
+ }),
73
+ z.object({
74
+ type: z.literal("customLink"),
75
+ url: z.string()
76
+ }),
77
+ z.object({
78
+ type: z.literal(
79
+ "errors.projectConnectedToBillingIntegrationForShopifyWorkspace"
80
+ )
81
+ }),
82
+ z.object({ type: z.literal("errors.themeAssetSizeLimitExceeded") }),
83
+ z.object({ type: z.literal("errors.themeTotalSizeLimitExceeded") }),
84
+ z.object({
85
+ type: z.literal("errors.pageMetafieldDefinitionsChanged")
86
+ }),
87
+ z.object({ type: z.literal("settings.billing") })
88
+ ])
89
+ })
90
+ ]).optional()
91
+ }).describe("ErrorUserFacingDetailsWithMessage");
92
+ z.discriminatedUnion("type", [
93
+ /**
94
+ * Normal errors, show error UI depending on the type
95
+ */
96
+ errorUserFacingDetailsMessageSchema,
97
+ /**
98
+ * Errors which should be ignored by the frontend. This is uncommon, usually
99
+ * you want to show an error in the UI, and if you don't want to then you catch
100
+ * the error in your route.
101
+ */
102
+ z.object({
103
+ type: z.literal("uncommon_none"),
104
+ key: z.string(),
105
+ usage: z.object({
106
+ current: z.number(),
107
+ maximum: z.number()
108
+ }).optional(),
109
+ message: z.string().optional(),
110
+ isRecoverable: z.boolean().optional(),
111
+ detail: z.array(z.string()).or(z.string()).optional()
112
+ })
113
+ ]).describe("ErrorUserFacingDetails");
114
+
115
+ export { ReploError };
@@ -0,0 +1,63 @@
1
+ // ../schemas/routing/match.ts
2
+ function matchSource({
3
+ source,
4
+ pathname
5
+ }) {
6
+ const trimmed = source.replace(/\/+$/, "") || "/";
7
+ let pattern;
8
+ try {
9
+ pattern = new URLPattern({ pathname: `${trimmed}{/}?` });
10
+ } catch {
11
+ return null;
12
+ }
13
+ const match = pattern.exec({
14
+ pathname: pathname.split(/[?#]/)[0] ?? pathname
15
+ });
16
+ if (!match) {
17
+ return null;
18
+ }
19
+ const params = {};
20
+ for (const [name, value] of Object.entries(match.pathname.groups)) {
21
+ if (/^\d+$/.test(name)) {
22
+ continue;
23
+ }
24
+ params[name] = value ?? "";
25
+ }
26
+ return params;
27
+ }
28
+ var ROUTE_PARAM = /:([A-Za-z0-9_.-]+?)([*+?]?)(?=[/?#]|$)/g;
29
+ function hasRouteParam(value) {
30
+ return new RegExp(ROUTE_PARAM.source).test(value);
31
+ }
32
+ function getRouteParams(value) {
33
+ const matches = value.matchAll(new RegExp(ROUTE_PARAM.source, "g"));
34
+ return [...matches].flatMap(([, name, modifier]) => {
35
+ return name ? [`${name}${modifier ?? ""}`] : [];
36
+ });
37
+ }
38
+ function applyParams({
39
+ value,
40
+ params
41
+ }) {
42
+ return value.replace(ROUTE_PARAM, (routeParam, name, modifier) => {
43
+ const captured = params[name];
44
+ if (captured !== void 0) {
45
+ return captured;
46
+ }
47
+ return modifier === "*" || modifier === "?" ? "" : routeParam;
48
+ });
49
+ }
50
+ var PARAM_SEGMENT = /^:([A-Za-z0-9_.-]+)([*+?]?)$/;
51
+ function getParamSegment(segment) {
52
+ const match = PARAM_SEGMENT.exec(segment);
53
+ if (!match?.[1]) {
54
+ return null;
55
+ }
56
+ const modifier = match[2];
57
+ return {
58
+ name: match[1],
59
+ modifier: modifier === "*" || modifier === "+" || modifier === "?" ? modifier : null
60
+ };
61
+ }
62
+
63
+ export { applyParams, getParamSegment, getRouteParams, hasRouteParam, matchSource };
package/env.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../env.ts"],
4
- "sourcesContent": ["import { getCloudflareContext } from \"@opennextjs/cloudflare\";\nimport JSONC from \"tiny-jsonc\";\nimport { z } from \"zod\";\n\nimport { CanopyError } from \"./lib/canopy-error\";\n\ntype Env = {\n CANOPY_API_HOST: string;\n PROJECT_ID: string;\n\n // Base URL of analytics-fire (e.g. https://data.replo.app). Read server-side by\n // the consent action (`consent/consent-actions.ts`) to forward proof-of-consent\n // records. Optional: absent in environments where the audit pipeline isn't\n // wired (e.g. local dev), where consent recording simply no-ops.\n ANALYTICS_FIRE_API_URL?: string;\n\n // Base URL of the Replo first-party analytics pixel CDN (e.g.\n // https://pixel.replo.app). `<ReploFirstPartyPixel>` loads\n // `${REPLO_ANALYTICS_PIXEL_URL}/latest.js` into the page once analytics consent\n // is granted; the browser fetches it directly. Optional: absent when the pixel\n // isn't configured (e.g. lean local dev), where the pixel simply isn't injected.\n REPLO_ANALYTICS_PIXEL_URL?: string;\n\n CHECKOUT_PROVIDER?: \"shopify\" | \"stripe\" | \"none\";\n};\n\nconst envSchema = z.object({\n CANOPY_API_HOST: z.string().min(1),\n PROJECT_ID: z.uuid(),\n ANALYTICS_FIRE_API_URL: z.string().optional(),\n REPLO_ANALYTICS_PIXEL_URL: z.string().optional(),\n CHECKOUT_PROVIDER: z.enum([\"shopify\", \"stripe\", \"none\"]).optional(),\n});\n\nconst wranglerConfigSchema = z.object({ vars: envSchema });\n\nclass MissingWranglerConfigError extends CanopyError {}\n\n// NOTE (Cole, 2026-07-28, REPL-29501): outside the deployed worker, read wrangler.jsonc directly \u2014 concurrent workerd boots race on the shared .wrangler/state SQLite (SQLITE_BUSY).\nconst isDeployedWorker = () => {\n return (\n process.env.NODE_ENV === \"production\" &&\n process.env.BUILD_ENV !== \"build\" &&\n process.env.NEXT_PHASE !== \"phase-production-build\"\n );\n};\n\nexport const getEnv = async (): Promise<Env> => {\n if (isDeployedWorker()) {\n const { env } = await getCloudflareContext({ async: true });\n return envSchema.parse(env);\n }\n // NOTE (Cole, 2026-07-29, REPL-29501): client chunks bundle this module (integration-utils has client consumers). A static node:fs broke those chunks in 0.16.3, and a computed specifier fails Turbopack too (\"expression is too dynamic\") \u2014 only the ignore-annotated literal import compiles; postbuild.mjs asserts the annotation survives into dist.\n const { promises: fs }: typeof import(\"node:fs\") = await import(\n /* turbopackIgnore: true */ /* webpackIgnore: true */ \"node:fs\"\n );\n let raw: string;\n try {\n raw = await fs.readFile(`${process.cwd()}/wrangler.jsonc`, \"utf8\");\n } catch (error) {\n throw new MissingWranglerConfigError({\n message:\n `wrangler.jsonc not found in ${process.cwd()}. It holds this site's Replo project config ` +\n `(project id, API host) and is required to run outside the deployed worker. ` +\n `If this clone predates the committed config, open the site in Replo once \u2014 the dev ` +\n `server regenerates and commits it \u2014 then git pull.`,\n cause: error,\n });\n }\n return wranglerConfigSchema.parse(JSONC.parse(raw)).vars;\n};\n"],
4
+ "sourcesContent": ["import { getCloudflareContext } from \"@opennextjs/cloudflare\";\nimport JSONC from \"tiny-jsonc\";\nimport { z } from \"zod\";\n\nimport { CanopyError } from \"./lib/canopy-error\";\n\ntype Env = {\n CANOPY_API_HOST: string;\n PROJECT_ID: string;\n\n // Base URL of analytics-fire (e.g. https://data.replo.app). Read server-side by\n // the consent action (`consent/consent-actions.ts`) to forward proof-of-consent\n // records. Optional: absent in environments where the audit pipeline isn't\n // wired (e.g. local dev), where consent recording simply no-ops.\n ANALYTICS_FIRE_API_URL?: string;\n\n // Base URL of the Replo first-party analytics pixel CDN (e.g.\n // https://pixel.replo.app). `<ReploFirstPartyPixel>` loads\n // `${REPLO_ANALYTICS_PIXEL_URL}/latest.js` into the page once analytics consent\n // is granted; the browser fetches it directly. Optional: absent when the pixel\n // isn't configured (e.g. lean local dev), where the pixel simply isn't injected.\n REPLO_ANALYTICS_PIXEL_URL?: string;\n\n CHECKOUT_PROVIDER?: \"shopify\" | \"stripe\" | \"none\";\n};\n\nconst envSchema = z.object({\n CANOPY_API_HOST: z.string().min(1),\n PROJECT_ID: z.uuid(),\n ANALYTICS_FIRE_API_URL: z.string().optional(),\n REPLO_ANALYTICS_PIXEL_URL: z.string().optional(),\n CHECKOUT_PROVIDER: z.enum([\"shopify\", \"stripe\", \"none\"]).optional(),\n});\n\nconst wranglerConfigSchema = z.object({ vars: envSchema });\n\nclass MissingWranglerConfigError extends CanopyError {}\n\n// NOTE (Cole, 2026-07-28, REPL-29501): outside the deployed worker, read wrangler.jsonc directly \u2014 concurrent workerd boots race on the shared .wrangler/state SQLite (SQLITE_BUSY).\nconst isDeployedWorker = () => {\n return (\n process.env.NODE_ENV === \"production\" &&\n process.env.BUILD_ENV !== \"build\" &&\n process.env.NEXT_PHASE !== \"phase-production-build\"\n );\n};\n\nexport const getEnv = async (): Promise<Env> => {\n if (isDeployedWorker()) {\n const { env } = await getCloudflareContext({ async: true });\n return envSchema.parse(env);\n }\n // NOTE (Cole, 2026-07-29, REPL-29501): client chunks bundle this module (integration-utils has client consumers). A static node:fs broke those chunks in 0.16.3, and a computed specifier fails Turbopack too (\"expression is too dynamic\") \u2014 only the ignore-annotated literal import compiles; postbuild.mts asserts the annotation survives into dist.\n const { promises: fs }: typeof import(\"node:fs\") = await import(\n /* turbopackIgnore: true */ /* webpackIgnore: true */ \"node:fs\"\n );\n let raw: string;\n try {\n raw = await fs.readFile(`${process.cwd()}/wrangler.jsonc`, \"utf8\");\n } catch (error) {\n throw new MissingWranglerConfigError({\n message:\n `wrangler.jsonc not found in ${process.cwd()}. It holds this site's Replo project config ` +\n `(project id, API host) and is required to run outside the deployed worker. ` +\n `If this clone predates the committed config, open the site in Replo once \u2014 the dev ` +\n `server regenerates and commits it \u2014 then git pull.`,\n cause: error,\n });\n }\n return wranglerConfigSchema.parse(JSONC.parse(raw)).vars;\n};\n"],
5
5
  "mappings": "AAAA,SAAS,4BAA4B;AACrC,OAAO,WAAW;AAClB,SAAS,SAAS;AAElB,SAAS,mBAAmB;AAsB5B,MAAM,YAAY,EAAE,OAAO;AAAA,EACzB,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,YAAY,EAAE,KAAK;AAAA,EACnB,wBAAwB,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5C,2BAA2B,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/C,mBAAmB,EAAE,KAAK,CAAC,WAAW,UAAU,MAAM,CAAC,EAAE,SAAS;AACpE,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEzD,MAAM,mCAAmC,YAAY;AAAC;AAGtD,MAAM,mBAAmB,MAAM;AAC7B,SACE,QAAQ,IAAI,aAAa,gBACzB,QAAQ,IAAI,cAAc,WAC1B,QAAQ,IAAI,eAAe;AAE/B;AAEO,MAAM,SAAS,YAA0B;AAC9C,MAAI,iBAAiB,GAAG;AACtB,UAAM,EAAE,IAAI,IAAI,MAAM,qBAAqB,EAAE,OAAO,KAAK,CAAC;AAC1D,WAAO,UAAU,MAAM,GAAG;AAAA,EAC5B;AAEA,QAAM,EAAE,UAAU,GAAG,IAA8B,MAAM;AAAA;AAAA;AAAA,IACD;AAAA,EACxD;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,IAAI,CAAC,mBAAmB,MAAM;AAAA,EACnE,SAAS,OAAO;AACd,UAAM,IAAI,2BAA2B;AAAA,MACnC,SACE,+BAA+B,QAAQ,IAAI,CAAC;AAAA,MAI9C,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACA,SAAO,qBAAqB,MAAM,MAAM,MAAM,GAAG,CAAC,EAAE;AACtD;",
6
6
  "names": []
7
7
  }
package/next/config.js CHANGED
@@ -17,6 +17,7 @@ const PLATFORM_SERVER_ACTION_ORIGINS = [
17
17
  ];
18
18
  function siteConfig(overrides = {}) {
19
19
  const isBuild = process.env.BUILD_ENV === "build";
20
+ const devRoot = process.env.IS_REPLO_SANDBOX ? "/workspace/dev" : process.cwd();
20
21
  const {
21
22
  turbopack: overrideTurbopack,
22
23
  images: overrideImages,
@@ -45,7 +46,7 @@ function siteConfig(overrides = {}) {
45
46
  // time; keep dev's watcher on the user app tree.
46
47
  turbopack: {
47
48
  ...overrideTurbopack,
48
- root: isBuild ? "/" : "/workspace/dev"
49
+ root: isBuild ? "/" : devRoot
49
50
  },
50
51
  // @replohq/sdk ships as raw source with zero workspace deps, so we can't pull
51
52
  // in lodash-es `uniq` here — dedupe inline via a Set instead.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../next/config.ts"],
4
- "sourcesContent": ["import os from \"os\";\nimport type { NextConfig } from \"next\";\n\n// Origins browsers load sandbox previews from \u2014 PREVIEW_HOSTNAME in the\n// coordinator's wrangler.jsonc plus Daytona's native preview domain. Next 16\n// hard-blocks cross-origin dev requests (HMR WebSocket, Server Actions) from\n// origins missing here.\nconst PLATFORM_DEV_ORIGINS = [\n \"localhost:*\",\n \"*.localhost:*\",\n \"*.lvh.me:*\",\n \"*.daytonaproxy01.net\",\n \"*.preview.reploapi.com\",\n \"*.preview-staging.reploapi.com\",\n];\n\nconst PLATFORM_SERVER_ACTION_ORIGINS = [\n \"localhost:*\",\n \"*.localhost:*\",\n \"*.lvh.me:*\",\n \"*.cloudflare-agent-coordinator.replo.workers.dev\",\n \"*.preview.reploapi.com\",\n \"*.preview-staging.reploapi.com\",\n];\n\n// The canonical Next.js runtime contract shared by every agent-built site.\n// Sites re-export `default` from here through a one-line `next.config.ts` shim,\n// so the platform wiring (sandbox dist dirs, Turbopack root, preview origins,\n// Cloudflare image loader) lives in exactly one place instead of being copied\n// into each template.\n//\n// `overrides` is a per-site customization surface, but it must not be able to\n// silently amputate platform wiring. A plain `...overrides` spread is a shallow\n// merge: a site that sets any nested key (e.g. `experimental.typedRoutes`)\n// would replace the whole `experimental`/`images` block and drop platform\n// defaults like the build-worker OOM guard or the Cloudflare image loader. So\n// load-bearing keys are re-asserted after the spread and the nested blocks are\n// deep-merged: a site can add keys, but platform invariants always win and\n// platform origin lists are preserved (site entries are appended, not swapped).\nexport function siteConfig(overrides: NextConfig = {}): NextConfig {\n const isBuild = process.env.BUILD_ENV === \"build\";\n const {\n turbopack: overrideTurbopack,\n images: overrideImages,\n experimental: overrideExperimental,\n allowedDevOrigins: overrideDevOrigins,\n ...restOverrides\n } = overrides;\n\n const { serverActions: overrideServerActions, ...restOverrideExperimental } =\n overrideExperimental ?? {};\n\n return {\n // Site-overridable defaults.\n reactStrictMode: true,\n devIndicators: false,\n ...restOverrides,\n\n // Platform invariants \u2014 re-asserted after the spread so a site override\n // can never break sandbox builds, publish, or the dev server.\n output: \"standalone\",\n distDir: isBuild ? \".next\" : \".next-dev\",\n // NOTE (Cole, 2026-04-30, REPL-26637): The publish tool patches\n // required-server-files.json to clear this value before OpenNext bundles,\n // since \"/\" makes esbuild emit absolute-path requires that break in Workers.\n // NOTE (Patrick Lu, 2026-07-06, REPL-28775): Leave outputFileTracingRoot\n // unset outside builds because Next 15.5+ otherwise forces turbopack.root to\n // \"/\" and makes the dev watcher retain whole-filesystem state.\n outputFileTracingRoot: isBuild ? \"/\" : undefined,\n // NOTE (Patrick Lu, 2026-07-06, REPL-28775): Match build tracing at publish\n // time; keep dev's watcher on the user app tree.\n turbopack: {\n ...overrideTurbopack,\n root: isBuild ? \"/\" : \"/workspace/dev\",\n },\n // @replohq/sdk ships as raw source with zero workspace deps, so we can't pull\n // in lodash-es `uniq` here \u2014 dedupe inline via a Set instead.\n allowedDevOrigins: Array.from(\n new Set([...(overrideDevOrigins ?? []), ...PLATFORM_DEV_ORIGINS]),\n ),\n images: {\n ...overrideImages,\n loader: \"custom\",\n loaderFile: \"./image-loader.ts\",\n remotePatterns: overrideImages?.remotePatterns ?? [\n {\n protocol: \"https\",\n hostname: \"**\",\n },\n ],\n },\n experimental: {\n ...restOverrideExperimental,\n // NOTE (Cole, 2026-06-18, REPL-28180): Next sizes its build worker pool\n // off os.cpus().length = host cores (~64) inside a cgroup-limited sandbox,\n // not the 2 the container actually has \u2014 a worker swarm that OOMs the\n // 4 GiB box. availableParallelism() is cgroup-aware. Gated to prod build.\n cpus: isBuild ? os.availableParallelism() : undefined,\n serverActions: {\n ...overrideServerActions,\n allowedOrigins: Array.from(\n new Set([\n ...(overrideServerActions?.allowedOrigins ?? []),\n ...PLATFORM_SERVER_ACTION_ORIGINS,\n ]),\n ),\n },\n },\n };\n}\n\nconst nextConfig: NextConfig = siteConfig();\n\nexport default nextConfig;\n"],
5
- "mappings": "AAAA,OAAO,QAAQ;AAOf,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,iCAAiC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgBO,SAAS,WAAW,YAAwB,CAAC,GAAe;AACjE,QAAM,UAAU,QAAQ,IAAI,cAAc;AAC1C,QAAM;AAAA,IACJ,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,EAAE,eAAe,uBAAuB,GAAG,yBAAyB,IACxE,wBAAwB,CAAC;AAE3B,SAAO;AAAA;AAAA,IAEL,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,GAAG;AAAA;AAAA;AAAA,IAIH,QAAQ;AAAA,IACR,SAAS,UAAU,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAO7B,uBAAuB,UAAU,MAAM;AAAA;AAAA;AAAA,IAGvC,WAAW;AAAA,MACT,GAAG;AAAA,MACH,MAAM,UAAU,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA,IAGA,mBAAmB,MAAM;AAAA,MACvB,oBAAI,IAAI,CAAC,GAAI,sBAAsB,CAAC,GAAI,GAAG,oBAAoB,CAAC;AAAA,IAClE;AAAA,IACA,QAAQ;AAAA,MACN,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,gBAAgB,gBAAgB,kBAAkB;AAAA,QAChD;AAAA,UACE,UAAU;AAAA,UACV,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,MAKH,MAAM,UAAU,GAAG,qBAAqB,IAAI;AAAA,MAC5C,eAAe;AAAA,QACb,GAAG;AAAA,QACH,gBAAgB,MAAM;AAAA,UACpB,oBAAI,IAAI;AAAA,YACN,GAAI,uBAAuB,kBAAkB,CAAC;AAAA,YAC9C,GAAG;AAAA,UACL,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,aAAyB,WAAW;AAE1C,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import os from \"os\";\nimport type { NextConfig } from \"next\";\n\n// Origins browsers load sandbox previews from \u2014 PREVIEW_HOSTNAME in the\n// coordinator's wrangler.jsonc plus Daytona's native preview domain. Next 16\n// hard-blocks cross-origin dev requests (HMR WebSocket, Server Actions) from\n// origins missing here.\nconst PLATFORM_DEV_ORIGINS = [\n \"localhost:*\",\n \"*.localhost:*\",\n \"*.lvh.me:*\",\n \"*.daytonaproxy01.net\",\n \"*.preview.reploapi.com\",\n \"*.preview-staging.reploapi.com\",\n];\n\nconst PLATFORM_SERVER_ACTION_ORIGINS = [\n \"localhost:*\",\n \"*.localhost:*\",\n \"*.lvh.me:*\",\n \"*.cloudflare-agent-coordinator.replo.workers.dev\",\n \"*.preview.reploapi.com\",\n \"*.preview-staging.reploapi.com\",\n];\n\n// The canonical Next.js runtime contract shared by every agent-built site.\n// Sites re-export `default` from here through a one-line `next.config.ts` shim,\n// so the platform wiring (sandbox dist dirs, Turbopack root, preview origins,\n// Cloudflare image loader) lives in exactly one place instead of being copied\n// into each template.\n//\n// `overrides` is a per-site customization surface, but it must not be able to\n// silently amputate platform wiring. A plain `...overrides` spread is a shallow\n// merge: a site that sets any nested key (e.g. `experimental.typedRoutes`)\n// would replace the whole `experimental`/`images` block and drop platform\n// defaults like the build-worker OOM guard or the Cloudflare image loader. So\n// load-bearing keys are re-asserted after the spread and the nested blocks are\n// deep-merged: a site can add keys, but platform invariants always win and\n// platform origin lists are preserved (site entries are appended, not swapped).\nexport function siteConfig(overrides: NextConfig = {}): NextConfig {\n const isBuild = process.env.BUILD_ENV === \"build\";\n const devRoot = process.env.IS_REPLO_SANDBOX\n ? \"/workspace/dev\"\n : process.cwd();\n const {\n turbopack: overrideTurbopack,\n images: overrideImages,\n experimental: overrideExperimental,\n allowedDevOrigins: overrideDevOrigins,\n ...restOverrides\n } = overrides;\n\n const { serverActions: overrideServerActions, ...restOverrideExperimental } =\n overrideExperimental ?? {};\n\n return {\n // Site-overridable defaults.\n reactStrictMode: true,\n devIndicators: false,\n ...restOverrides,\n\n // Platform invariants \u2014 re-asserted after the spread so a site override\n // can never break sandbox builds, publish, or the dev server.\n output: \"standalone\",\n distDir: isBuild ? \".next\" : \".next-dev\",\n // NOTE (Cole, 2026-04-30, REPL-26637): The publish tool patches\n // required-server-files.json to clear this value before OpenNext bundles,\n // since \"/\" makes esbuild emit absolute-path requires that break in Workers.\n // NOTE (Patrick Lu, 2026-07-06, REPL-28775): Leave outputFileTracingRoot\n // unset outside builds because Next 15.5+ otherwise forces turbopack.root to\n // \"/\" and makes the dev watcher retain whole-filesystem state.\n outputFileTracingRoot: isBuild ? \"/\" : undefined,\n // NOTE (Patrick Lu, 2026-07-06, REPL-28775): Match build tracing at publish\n // time; keep dev's watcher on the user app tree.\n turbopack: {\n ...overrideTurbopack,\n root: isBuild ? \"/\" : devRoot,\n },\n // @replohq/sdk ships as raw source with zero workspace deps, so we can't pull\n // in lodash-es `uniq` here \u2014 dedupe inline via a Set instead.\n allowedDevOrigins: Array.from(\n new Set([...(overrideDevOrigins ?? []), ...PLATFORM_DEV_ORIGINS]),\n ),\n images: {\n ...overrideImages,\n loader: \"custom\",\n loaderFile: \"./image-loader.ts\",\n remotePatterns: overrideImages?.remotePatterns ?? [\n {\n protocol: \"https\",\n hostname: \"**\",\n },\n ],\n },\n experimental: {\n ...restOverrideExperimental,\n // NOTE (Cole, 2026-06-18, REPL-28180): Next sizes its build worker pool\n // off os.cpus().length = host cores (~64) inside a cgroup-limited sandbox,\n // not the 2 the container actually has \u2014 a worker swarm that OOMs the\n // 4 GiB box. availableParallelism() is cgroup-aware. Gated to prod build.\n cpus: isBuild ? os.availableParallelism() : undefined,\n serverActions: {\n ...overrideServerActions,\n allowedOrigins: Array.from(\n new Set([\n ...(overrideServerActions?.allowedOrigins ?? []),\n ...PLATFORM_SERVER_ACTION_ORIGINS,\n ]),\n ),\n },\n },\n };\n}\n\nconst nextConfig: NextConfig = siteConfig();\n\nexport default nextConfig;\n"],
5
+ "mappings": "AAAA,OAAO,QAAQ;AAOf,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,iCAAiC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAgBO,SAAS,WAAW,YAAwB,CAAC,GAAe;AACjE,QAAM,UAAU,QAAQ,IAAI,cAAc;AAC1C,QAAM,UAAU,QAAQ,IAAI,mBACxB,mBACA,QAAQ,IAAI;AAChB,QAAM;AAAA,IACJ,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,mBAAmB;AAAA,IACnB,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,EAAE,eAAe,uBAAuB,GAAG,yBAAyB,IACxE,wBAAwB,CAAC;AAE3B,SAAO;AAAA;AAAA,IAEL,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,GAAG;AAAA;AAAA;AAAA,IAIH,QAAQ;AAAA,IACR,SAAS,UAAU,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAO7B,uBAAuB,UAAU,MAAM;AAAA;AAAA;AAAA,IAGvC,WAAW;AAAA,MACT,GAAG;AAAA,MACH,MAAM,UAAU,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA,IAGA,mBAAmB,MAAM;AAAA,MACvB,oBAAI,IAAI,CAAC,GAAI,sBAAsB,CAAC,GAAI,GAAG,oBAAoB,CAAC;AAAA,IAClE;AAAA,IACA,QAAQ;AAAA,MACN,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,gBAAgB,gBAAgB,kBAAkB;AAAA,QAChD;AAAA,UACE,UAAU;AAAA,UACV,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,cAAc;AAAA,MACZ,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,MAKH,MAAM,UAAU,GAAG,qBAAqB,IAAI;AAAA,MAC5C,eAAe;AAAA,QACb,GAAG;AAAA,QACH,gBAAgB,MAAM;AAAA,UACpB,oBAAI,IAAI;AAAA,YACN,GAAI,uBAAuB,kBAAkB,CAAC;AAAA,YAC9C,GAAG;AAAA,UACL,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,aAAyB,WAAW;AAE1C,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -186,6 +186,14 @@
186
186
  "types": "./next/open-next.d.ts",
187
187
  "default": "./next/open-next.js"
188
188
  },
189
+ "./routing/evaluate": {
190
+ "types": "./routing/evaluate.d.ts",
191
+ "default": "./routing/evaluate.js"
192
+ },
193
+ "./routing/rules": {
194
+ "types": "./routing/rules.d.ts",
195
+ "default": "./routing/rules.js"
196
+ },
189
197
  "./_vendor/schemas/loaderKeys": {
190
198
  "types": "./_vendor/schemas/loaderKeys.d.ts",
191
199
  "default": "./_vendor/schemas/loaderKeys.mjs"
@@ -0,0 +1,7 @@
1
+ import type { NextRequest } from "next/server";
2
+ import type { RedirectRule } from "../_vendor/schemas/routing/rules";
3
+ import { NextResponse } from "next/server";
4
+ export declare function evaluateRouting({ request, rules, }: {
5
+ request: NextRequest;
6
+ rules: readonly RedirectRule[];
7
+ }): NextResponse;
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from "next/server";
2
+ import { applyParams, matchSource } from "../_vendor/schemas/routing/match.mjs";
3
+ function evaluateRouting({
4
+ request,
5
+ rules
6
+ }) {
7
+ for (const rule of rules) {
8
+ const response = evaluateRedirect({ request, rule });
9
+ if (response) {
10
+ return response;
11
+ }
12
+ }
13
+ return NextResponse.next();
14
+ }
15
+ function evaluateRedirect({
16
+ request,
17
+ rule
18
+ }) {
19
+ const params = matchSource({
20
+ source: rule.source,
21
+ pathname: request.nextUrl.pathname
22
+ });
23
+ if (!params) {
24
+ return null;
25
+ }
26
+ const url = new URL(
27
+ applyParams({ value: rule.destination, params }),
28
+ request.nextUrl.origin
29
+ );
30
+ if (url.origin === request.nextUrl.origin && !url.search) {
31
+ url.search = request.nextUrl.search;
32
+ }
33
+ const redirectsToCurrentRequest = url.origin === request.nextUrl.origin && normalizePath(url.pathname) === normalizePath(request.nextUrl.pathname) && url.search === request.nextUrl.search;
34
+ if (redirectsToCurrentRequest) {
35
+ return NextResponse.next();
36
+ }
37
+ return NextResponse.redirect(url, rule.status);
38
+ }
39
+ function normalizePath(pathname) {
40
+ const trimmed = pathname.replace(/\/+$/, "");
41
+ return trimmed === "" ? "/" : trimmed;
42
+ }
43
+ export {
44
+ evaluateRouting
45
+ };
46
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../routing/evaluate.ts"],
4
+ "sourcesContent": ["import type { NextRequest } from \"next/server\";\nimport type { RedirectRule } from \"schemas/routing/rules\";\n\nimport { NextResponse } from \"next/server\";\n\nimport { applyParams, matchSource } from \"schemas/routing/match\";\n\nexport function evaluateRouting({\n request,\n rules,\n}: {\n request: NextRequest;\n rules: readonly RedirectRule[];\n}): NextResponse {\n for (const rule of rules) {\n const response = evaluateRedirect({ request, rule });\n if (response) {\n return response;\n }\n }\n return NextResponse.next();\n}\n\nfunction evaluateRedirect({\n request,\n rule,\n}: {\n request: NextRequest;\n rule: RedirectRule;\n}): NextResponse | null {\n const params = matchSource({\n source: rule.source,\n pathname: request.nextUrl.pathname,\n });\n if (!params) {\n return null;\n }\n const url = new URL(\n applyParams({ value: rule.destination, params }),\n request.nextUrl.origin,\n );\n if (url.origin === request.nextUrl.origin && !url.search) {\n url.search = request.nextUrl.search;\n }\n // A destination that lands back on the current request would loop. The rule\n // still consumes the request \u2014 later rules were authored as alternatives to\n // it, not fallbacks for it \u2014 so the page renders normally instead.\n const redirectsToCurrentRequest =\n url.origin === request.nextUrl.origin &&\n normalizePath(url.pathname) === normalizePath(request.nextUrl.pathname) &&\n url.search === request.nextUrl.search;\n if (redirectsToCurrentRequest) {\n return NextResponse.next();\n }\n return NextResponse.redirect(url, rule.status);\n}\n\n/** Trailing slashes carry no meaning, so `/blogs` and `/blogs/` are the same. */\nfunction normalizePath(pathname: string): string {\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n"],
5
+ "mappings": "AAGA,SAAS,oBAAoB;AAE7B,SAAS,aAAa,mBAAmB;AAElC,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AACF,GAGiB;AACf,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,iBAAiB,EAAE,SAAS,KAAK,CAAC;AACnD,QAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,aAAa,KAAK;AAC3B;AAEA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA;AACF,GAGwB;AACtB,QAAM,SAAS,YAAY;AAAA,IACzB,QAAQ,KAAK;AAAA,IACb,UAAU,QAAQ,QAAQ;AAAA,EAC5B,CAAC;AACD,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AACA,QAAM,MAAM,IAAI;AAAA,IACd,YAAY,EAAE,OAAO,KAAK,aAAa,OAAO,CAAC;AAAA,IAC/C,QAAQ,QAAQ;AAAA,EAClB;AACA,MAAI,IAAI,WAAW,QAAQ,QAAQ,UAAU,CAAC,IAAI,QAAQ;AACxD,QAAI,SAAS,QAAQ,QAAQ;AAAA,EAC/B;AAIA,QAAM,4BACJ,IAAI,WAAW,QAAQ,QAAQ,UAC/B,cAAc,IAAI,QAAQ,MAAM,cAAc,QAAQ,QAAQ,QAAQ,KACtE,IAAI,WAAW,QAAQ,QAAQ;AACjC,MAAI,2BAA2B;AAC7B,WAAO,aAAa,KAAK;AAAA,EAC3B;AACA,SAAO,aAAa,SAAS,KAAK,KAAK,MAAM;AAC/C;AAGA,SAAS,cAAc,UAA0B;AAC/C,QAAM,UAAU,SAAS,QAAQ,QAAQ,EAAE;AAC3C,SAAO,YAAY,KAAK,MAAM;AAChC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Site-facing entrypoint for redirect rule authoring.
3
+ *
4
+ * The schema, validators, and matcher live in `schemas/routing` so the Website
5
+ * Builder can import them without depending on a published package, whose
6
+ * release cycle shouldn't gate editor-only changes. Sites can't import
7
+ * `schemas` directly — it is workspace-only and reaches generated sites through
8
+ * the SDK's vendoring step — so `middleware.ts` keeps importing `reploRedirect`
9
+ * from here.
10
+ */
11
+ export type { RedirectRule, RedirectStatus, } from "../_vendor/schemas/routing/rules";
12
+ export { reploRedirect } from "../_vendor/schemas/routing/rules";
@@ -0,0 +1,5 @@
1
+ import { reploRedirect } from "../_vendor/schemas/routing/rules.mjs";
2
+ export {
3
+ reploRedirect
4
+ };
5
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../routing/rules.ts"],
4
+ "sourcesContent": ["/**\n * Site-facing entrypoint for redirect rule authoring.\n *\n * The schema, validators, and matcher live in `schemas/routing` so the Website\n * Builder can import them without depending on a published package, whose\n * release cycle shouldn't gate editor-only changes. Sites can't import\n * `schemas` directly \u2014 it is workspace-only and reaches generated sites through\n * the SDK's vendoring step \u2014 so `middleware.ts` keeps importing `reploRedirect`\n * from here.\n */\nexport type {\n RedirectRule,\n RedirectStatus,\n} from \"schemas/routing/rules\"; /* eslint-disable-line replo/no-export-from -- site-facing entrypoint intentionally re-exports the authoring API. */\nexport { reploRedirect } from \"schemas/routing/rules\"; /* eslint-disable-line replo/no-export-from -- site-facing entrypoint intentionally re-exports the authoring API. */\n"],
5
+ "mappings": "AAcA,SAAS,qBAAqB;",
6
+ "names": []
7
+ }