@nuxt/scripts 1.0.0-rc.5 → 1.0.0-rc.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/bin/cli.mjs +2 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +50 -0
- package/dist/devtools-client/200.html +1 -1
- package/dist/devtools-client/404.html +1 -1
- package/dist/devtools-client/_nuxt/{CL6TeQIJ.js → 5D-5agUu.js} +31 -31
- package/dist/devtools-client/_nuxt/{DoF6byDH.js → BDlZgWHO.js} +1 -1
- package/dist/devtools-client/_nuxt/{D838xXrH.js → BntLcF3H.js} +1 -1
- package/dist/devtools-client/_nuxt/{BFtOB2Ap.js → CC9d18RE.js} +1 -1
- package/dist/devtools-client/_nuxt/{BfQVeAez.js → CaQ1scfO.js} +1 -1
- package/dist/devtools-client/_nuxt/{C7-YRs3P.js → DJ5bfe9v.js} +1 -1
- package/dist/devtools-client/_nuxt/{Duf9abe1.js → YKhzFESo.js} +1 -1
- package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
- package/dist/devtools-client/_nuxt/builds/meta/7a96fd5e-d239-4ba5-816b-05034a861ba0.json +1 -0
- package/dist/devtools-client/_nuxt/error-404.Dwj0Wlzm.css +1 -0
- package/dist/devtools-client/_nuxt/error-500.B4wHUYBa.css +1 -0
- package/dist/devtools-client/docs/index.html +1 -1
- package/dist/devtools-client/first-party/index.html +1 -1
- package/dist/devtools-client/index.html +1 -1
- package/dist/devtools-client/registry/index.html +1 -1
- package/dist/module.d.mts +51 -2
- package/dist/module.d.ts +51 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +80 -9
- package/dist/registry.mjs +10 -10
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.d.vue.ts +73 -93
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +65 -57
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue.d.ts +73 -93
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.d.vue.ts +49 -28
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue +143 -103
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue.d.ts +49 -28
- package/dist/runtime/components/GoogleMaps/useGoogleMapsResource.d.ts +50 -0
- package/dist/runtime/components/GoogleMaps/useGoogleMapsResource.js +76 -1
- package/dist/runtime/server/bluesky-embed.js +3 -2
- package/dist/runtime/server/google-maps-geocode-proxy.js +3 -2
- package/dist/runtime/server/google-static-maps-proxy.js +3 -2
- package/dist/runtime/server/gravatar-proxy.js +3 -2
- package/dist/runtime/server/instagram-embed.js +3 -2
- package/dist/runtime/server/utils/image-proxy.js +3 -2
- package/dist/runtime/server/utils/sign.d.ts +109 -0
- package/dist/runtime/server/utils/sign.js +88 -0
- package/dist/runtime/server/utils/withSigning.d.ts +23 -0
- package/dist/runtime/server/utils/withSigning.js +18 -0
- package/dist/runtime/server/x-embed.js +3 -2
- package/dist/runtime/types.d.ts +9 -1
- package/dist/types-source.mjs +2 -198
- package/dist/types.d.mts +2 -2
- package/package.json +6 -2
- package/dist/devtools-client/_nuxt/builds/meta/03a9e297-6209-4281-8c3d-4265bdd5d038.json +0 -1
- package/dist/devtools-client/_nuxt/error-404.BGYgyi5Q.css +0 -1
- package/dist/devtools-client/_nuxt/error-500.7RpV0mcx.css +0 -1
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
type
|
|
2
|
-
type
|
|
3
|
-
type
|
|
1
|
+
import type { ShallowRef } from 'vue';
|
|
2
|
+
export type ScriptGoogleMapsOverlayAnchor = 'center' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' | 'left-center' | 'right-center';
|
|
3
|
+
export type ScriptGoogleMapsOverlayPane = 'mapPane' | 'overlayLayer' | 'markerLayer' | 'overlayMouseTarget' | 'floatPane';
|
|
4
|
+
export interface ScriptGoogleMapsOverlayViewProps {
|
|
4
5
|
/**
|
|
5
6
|
* Geographic position for the overlay. Falls back to parent marker position if omitted.
|
|
7
|
+
*
|
|
8
|
+
* Accepts either a plain `LatLngLiteral` (`{ lat, lng }`) or a
|
|
9
|
+
* `google.maps.LatLng` instance.
|
|
6
10
|
* @see https://developers.google.com/maps/documentation/javascript/reference/overlay-view#OverlayView
|
|
7
11
|
*/
|
|
8
|
-
position?: google.maps.LatLngLiteral;
|
|
12
|
+
position?: google.maps.LatLng | google.maps.LatLngLiteral;
|
|
13
|
+
/**
|
|
14
|
+
* Initial open state for the uncontrolled mode (when `v-model:open` is not
|
|
15
|
+
* bound). When omitted, the overlay opens on mount, matching v0 behaviour.
|
|
16
|
+
*
|
|
17
|
+
* Has no effect when `v-model:open` is used; pass an initial value to the
|
|
18
|
+
* bound ref instead.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
defaultOpen?: boolean;
|
|
9
22
|
/**
|
|
10
23
|
* Anchor point of the overlay relative to its position.
|
|
11
24
|
* @default 'bottom-center'
|
|
12
25
|
*/
|
|
13
|
-
anchor?:
|
|
26
|
+
anchor?: ScriptGoogleMapsOverlayAnchor;
|
|
14
27
|
/**
|
|
15
28
|
* Pixel offset from the anchor position.
|
|
16
29
|
*/
|
|
@@ -23,7 +36,7 @@ type __VLS_Props = {
|
|
|
23
36
|
* @default 'floatPane'
|
|
24
37
|
* @see https://developers.google.com/maps/documentation/javascript/reference/overlay-view#MapPanes
|
|
25
38
|
*/
|
|
26
|
-
pane?:
|
|
39
|
+
pane?: ScriptGoogleMapsOverlayPane;
|
|
27
40
|
/**
|
|
28
41
|
* CSS z-index for the overlay element.
|
|
29
42
|
*/
|
|
@@ -45,31 +58,39 @@ type __VLS_Props = {
|
|
|
45
58
|
* @default true
|
|
46
59
|
*/
|
|
47
60
|
hideWhenClustered?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
61
|
+
}
|
|
62
|
+
export interface ScriptGoogleMapsOverlayViewEmits {
|
|
63
|
+
/** Event handler called when the open state of the overlay view changes. */
|
|
64
|
+
'update:open': [value: boolean];
|
|
65
|
+
}
|
|
66
|
+
export interface ScriptGoogleMapsOverlayViewSlots {
|
|
50
67
|
default?: () => any;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
blockMapInteraction: boolean;
|
|
67
|
-
panOnOpen: boolean | number;
|
|
68
|
-
hideWhenClustered: boolean;
|
|
69
|
-
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
70
|
-
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
68
|
+
}
|
|
69
|
+
export interface ScriptGoogleMapsOverlayViewExpose {
|
|
70
|
+
/** The underlying `OverlayView` instance. */
|
|
71
|
+
overlayView: ShallowRef<google.maps.OverlayView | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* The underlying `OverlayView` instance.
|
|
74
|
+
*
|
|
75
|
+
* @deprecated Use `overlayView` instead. The `overlay` alias will be
|
|
76
|
+
* removed in a future major version.
|
|
77
|
+
* @see https://scripts.nuxt.com/docs/migration-guide/v0-to-v1
|
|
78
|
+
*/
|
|
79
|
+
overlay: ShallowRef<google.maps.OverlayView | undefined>;
|
|
80
|
+
/** The current data-state of the overlay, either 'open' or 'closed'. */
|
|
81
|
+
dataState: Readonly<ShallowRef<'open' | 'closed'>>;
|
|
82
|
+
}
|
|
71
83
|
declare const _default: typeof __VLS_export;
|
|
72
84
|
export default _default;
|
|
85
|
+
declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<ScriptGoogleMapsOverlayViewProps & {
|
|
86
|
+
open?: boolean;
|
|
87
|
+
}, ScriptGoogleMapsOverlayViewExpose, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
88
|
+
"update:open": (value: boolean) => any;
|
|
89
|
+
}, string, import("vue").PublicProps, Readonly<ScriptGoogleMapsOverlayViewProps & {
|
|
90
|
+
open?: boolean;
|
|
91
|
+
}> & Readonly<{
|
|
92
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
93
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, ScriptGoogleMapsOverlayViewSlots>;
|
|
73
94
|
type __VLS_WithSlots<T, S> = T & {
|
|
74
95
|
new (): {
|
|
75
96
|
$slots: S;
|
|
@@ -25,6 +25,56 @@ export interface GoogleMapsResourceContext {
|
|
|
25
25
|
map: google.maps.Map;
|
|
26
26
|
mapsApi: typeof google.maps;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalizes a `LatLng | LatLngLiteral` value into a plain `LatLngLiteral`.
|
|
30
|
+
*
|
|
31
|
+
* Google's `LatLng` exposes coordinates via `.lat()`/`.lng()` methods, while
|
|
32
|
+
* `LatLngLiteral` exposes them as plain `lat`/`lng` numeric properties. The
|
|
33
|
+
* runtime distinguishes them by checking whether `.lat` is callable; this is
|
|
34
|
+
* preferred over `instanceof google.maps.LatLng` because mocked APIs in tests
|
|
35
|
+
* return plain objects rather than real `LatLng` instances.
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeLatLng(p: google.maps.LatLng | google.maps.LatLngLiteral): google.maps.LatLngLiteral;
|
|
38
|
+
/**
|
|
39
|
+
* Defines a deprecated property alias on an exposed object. Reading the alias
|
|
40
|
+
* returns the value of the canonical key and emits a one-shot
|
|
41
|
+
* `console.warn` (so repeated reads don't spam the console).
|
|
42
|
+
*
|
|
43
|
+
* Used to provide backward-compatible renames on `defineExpose` payloads
|
|
44
|
+
* without breaking existing template-ref consumers. Call sites should wrap
|
|
45
|
+
* this in `if (import.meta.dev)` so production builds skip the getter
|
|
46
|
+
* entirely and the alias stays a plain data property.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineDeprecatedAlias<T extends object, K extends keyof T>(target: T, alias: string, canonicalKey: K, message: string): T;
|
|
49
|
+
/**
|
|
50
|
+
* Emits dev-mode deprecation warnings for the legacy top-level `center` and
|
|
51
|
+
* `zoom` props on `<ScriptGoogleMaps>`. Both props still work, but new code
|
|
52
|
+
* should pass them via `mapOptions` instead.
|
|
53
|
+
*
|
|
54
|
+
* Returns the number of warnings emitted (useful for tests).
|
|
55
|
+
*/
|
|
56
|
+
export declare function warnDeprecatedTopLevelMapProps(props: {
|
|
57
|
+
center?: unknown;
|
|
58
|
+
zoom?: unknown;
|
|
59
|
+
}): number;
|
|
60
|
+
/**
|
|
61
|
+
* Wait until the Google Maps API and a Map instance are both available.
|
|
62
|
+
*
|
|
63
|
+
* Triggers script loading via `load()` if not already loaded. Uses an
|
|
64
|
+
* immediate watcher (matching `importLibrary`'s pattern) to avoid the race
|
|
65
|
+
* where `load()` resolves synchronously: a non-immediate watcher would miss
|
|
66
|
+
* the change and the promise would hang forever.
|
|
67
|
+
*
|
|
68
|
+
* Rejects if `status` enters an `'error'` state before both refs are populated.
|
|
69
|
+
* Runs the watcher inside a detached effect scope so it is safe to call from
|
|
70
|
+
* any context (component setup, exposed methods, tests).
|
|
71
|
+
*/
|
|
72
|
+
export declare function waitForMapsReady({ mapsApi, map, status, load, }: {
|
|
73
|
+
mapsApi: ShallowRef<typeof google.maps | undefined>;
|
|
74
|
+
map: ShallowRef<google.maps.Map | undefined>;
|
|
75
|
+
status: Ref<string>;
|
|
76
|
+
load: () => Promise<unknown> | unknown;
|
|
77
|
+
}): Promise<void>;
|
|
28
78
|
/**
|
|
29
79
|
* Composable for safely managing Google Maps resource lifecycle.
|
|
30
80
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { whenever } from "@vueuse/core";
|
|
2
|
-
import { inject, onUnmounted, ref, shallowRef } from "vue";
|
|
2
|
+
import { effectScope, inject, onUnmounted, ref, shallowRef, watch } from "vue";
|
|
3
3
|
export const MAP_INJECTION_KEY = Symbol("map");
|
|
4
4
|
export const MARKER_INJECTION_KEY = Symbol("marker");
|
|
5
5
|
export function bindGoogleMapsEvents(instance, emit, config) {
|
|
@@ -10,6 +10,81 @@ export function bindGoogleMapsEvents(instance, emit, config) {
|
|
|
10
10
|
instance.addListener(event, (payload) => emit(event, payload));
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
+
export function normalizeLatLng(p) {
|
|
14
|
+
if (typeof p.lat === "function") {
|
|
15
|
+
const ll = p;
|
|
16
|
+
return { lat: ll.lat(), lng: ll.lng() };
|
|
17
|
+
}
|
|
18
|
+
return { lat: p.lat, lng: p.lng };
|
|
19
|
+
}
|
|
20
|
+
export function defineDeprecatedAlias(target, alias, canonicalKey, message) {
|
|
21
|
+
let warned = false;
|
|
22
|
+
Object.defineProperty(target, alias, {
|
|
23
|
+
get() {
|
|
24
|
+
if (!warned) {
|
|
25
|
+
warned = true;
|
|
26
|
+
console.warn(message);
|
|
27
|
+
}
|
|
28
|
+
return target[canonicalKey];
|
|
29
|
+
},
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true
|
|
32
|
+
});
|
|
33
|
+
return target;
|
|
34
|
+
}
|
|
35
|
+
export function warnDeprecatedTopLevelMapProps(props) {
|
|
36
|
+
let warned = 0;
|
|
37
|
+
if (props.center !== void 0) {
|
|
38
|
+
warned++;
|
|
39
|
+
console.warn(
|
|
40
|
+
'[nuxt-scripts] <ScriptGoogleMaps> prop "center" is deprecated; use `:map-options="{ center: ... }"` instead. See https://scripts.nuxt.com/docs/migration-guide/v0-to-v1'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (props.zoom !== void 0) {
|
|
44
|
+
warned++;
|
|
45
|
+
console.warn(
|
|
46
|
+
'[nuxt-scripts] <ScriptGoogleMaps> prop "zoom" is deprecated; use `:map-options="{ zoom: ... }"` instead. See https://scripts.nuxt.com/docs/migration-guide/v0-to-v1'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return warned;
|
|
50
|
+
}
|
|
51
|
+
export async function waitForMapsReady({
|
|
52
|
+
mapsApi,
|
|
53
|
+
map,
|
|
54
|
+
status,
|
|
55
|
+
load
|
|
56
|
+
}) {
|
|
57
|
+
if (mapsApi.value && map.value)
|
|
58
|
+
return;
|
|
59
|
+
if (status.value === "error")
|
|
60
|
+
throw new Error("Google Maps script failed to load");
|
|
61
|
+
await load();
|
|
62
|
+
if (mapsApi.value && map.value)
|
|
63
|
+
return;
|
|
64
|
+
if (status.value === "error")
|
|
65
|
+
throw new Error("Google Maps script failed to load");
|
|
66
|
+
const scope = effectScope(true);
|
|
67
|
+
try {
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
scope.run(() => {
|
|
70
|
+
watch(
|
|
71
|
+
[mapsApi, map, status],
|
|
72
|
+
([api, m, s]) => {
|
|
73
|
+
if (api && m) {
|
|
74
|
+
resolve();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (s === "error")
|
|
78
|
+
reject(new Error("Google Maps script failed to load"));
|
|
79
|
+
},
|
|
80
|
+
{ immediate: true }
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
scope.stop();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
13
88
|
export function useGoogleMapsResource({
|
|
14
89
|
ready,
|
|
15
90
|
create,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
3
4
|
const BSKY_POST_URL_RE = /^https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/?]+)$/;
|
|
4
|
-
export default defineEventHandler(async (event) => {
|
|
5
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
5
6
|
const query = getQuery(event);
|
|
6
7
|
const postUrl = query.url;
|
|
7
8
|
if (!postUrl) {
|
|
@@ -56,4 +57,4 @@ export default defineEventHandler(async (event) => {
|
|
|
56
57
|
setHeader(event, "Content-Type", "application/json");
|
|
57
58
|
setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
|
|
58
59
|
return post;
|
|
59
|
-
});
|
|
60
|
+
}));
|
|
@@ -2,7 +2,8 @@ import { useRuntimeConfig } from "#imports";
|
|
|
2
2
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
3
3
|
import { $fetch } from "ofetch";
|
|
4
4
|
import { withQuery } from "ufo";
|
|
5
|
-
|
|
5
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
6
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
6
7
|
const runtimeConfig = useRuntimeConfig();
|
|
7
8
|
const privateConfig = runtimeConfig["nuxt-scripts"]?.googleMapsGeocodeProxy;
|
|
8
9
|
const apiKey = privateConfig?.apiKey;
|
|
@@ -31,4 +32,4 @@ export default defineEventHandler(async (event) => {
|
|
|
31
32
|
setHeader(event, "Content-Type", "application/json");
|
|
32
33
|
setHeader(event, "Cache-Control", "public, max-age=86400, s-maxage=86400");
|
|
33
34
|
return data;
|
|
34
|
-
});
|
|
35
|
+
}));
|
|
@@ -2,7 +2,8 @@ import { useRuntimeConfig } from "#imports";
|
|
|
2
2
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
3
3
|
import { $fetch } from "ofetch";
|
|
4
4
|
import { withQuery } from "ufo";
|
|
5
|
-
|
|
5
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
6
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
6
7
|
const runtimeConfig = useRuntimeConfig();
|
|
7
8
|
const publicConfig = runtimeConfig.public["nuxt-scripts"]?.googleStaticMapsProxy;
|
|
8
9
|
const privateConfig = runtimeConfig["nuxt-scripts"]?.googleStaticMapsProxy;
|
|
@@ -40,4 +41,4 @@ export default defineEventHandler(async (event) => {
|
|
|
40
41
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
41
42
|
setHeader(event, "Vary", "Accept-Encoding");
|
|
42
43
|
return response._data;
|
|
43
|
-
});
|
|
44
|
+
}));
|
|
@@ -2,7 +2,8 @@ import { useRuntimeConfig } from "#imports";
|
|
|
2
2
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
3
3
|
import { $fetch } from "ofetch";
|
|
4
4
|
import { withQuery } from "ufo";
|
|
5
|
-
|
|
5
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
6
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
6
7
|
const runtimeConfig = useRuntimeConfig();
|
|
7
8
|
const proxyConfig = runtimeConfig.public["nuxt-scripts"]?.gravatarProxy;
|
|
8
9
|
const query = getQuery(event);
|
|
@@ -43,4 +44,4 @@ export default defineEventHandler(async (event) => {
|
|
|
43
44
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
44
45
|
setHeader(event, "Vary", "Accept-Encoding");
|
|
45
46
|
return response._data;
|
|
46
|
-
});
|
|
47
|
+
}));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
3
|
import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
|
|
4
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
4
5
|
export const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
|
|
5
6
|
export const AMP_RE = /&/g;
|
|
6
7
|
export const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
|
|
@@ -124,7 +125,7 @@ function extractBlock(css, openBrace) {
|
|
|
124
125
|
}
|
|
125
126
|
return null;
|
|
126
127
|
}
|
|
127
|
-
export default defineEventHandler(async (event) => {
|
|
128
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
128
129
|
const handlerPath = event.path?.split("?")[0] || "";
|
|
129
130
|
const prefix = handlerPath.replace(EMBED_INSTAGRAM_SUFFIX_RE, "") || "/_scripts";
|
|
130
131
|
const query = getQuery(event);
|
|
@@ -229,4 +230,4 @@ ${combinedCss}</style>${bodyHtml}</div>`;
|
|
|
229
230
|
setHeader(event, "Content-Type", "text/html");
|
|
230
231
|
setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
|
|
231
232
|
return result;
|
|
232
|
-
});
|
|
233
|
+
}));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
|
+
import { withSigning } from "./withSigning.js";
|
|
3
4
|
const AMP_RE = /&/g;
|
|
4
5
|
export function createImageProxyHandler(config) {
|
|
5
6
|
const {
|
|
@@ -10,7 +11,7 @@ export function createImageProxyHandler(config) {
|
|
|
10
11
|
followRedirects = true,
|
|
11
12
|
decodeAmpersands = false
|
|
12
13
|
} = config;
|
|
13
|
-
return defineEventHandler(async (event) => {
|
|
14
|
+
return withSigning(defineEventHandler(async (event) => {
|
|
14
15
|
const query = getQuery(event);
|
|
15
16
|
let url = query.url;
|
|
16
17
|
if (decodeAmpersands && url)
|
|
@@ -66,5 +67,5 @@ export function createImageProxyHandler(config) {
|
|
|
66
67
|
setHeader(event, "Content-Type", response.headers.get("content-type") || contentType);
|
|
67
68
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
68
69
|
return response._data;
|
|
69
|
-
});
|
|
70
|
+
}));
|
|
70
71
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC URL signing for proxy endpoints.
|
|
3
|
+
*
|
|
4
|
+
* ## Why
|
|
5
|
+
*
|
|
6
|
+
* Proxy endpoints like `/_scripts/proxy/google-static-maps` inject a server-side
|
|
7
|
+
* API key and forward requests to third-party services. Without signing, anyone
|
|
8
|
+
* can call these endpoints with arbitrary parameters and burn the site owner's
|
|
9
|
+
* API quota. Signing ensures only URLs generated server-side (during SSR/prerender
|
|
10
|
+
* or via the `/_scripts/sign` endpoint) are accepted.
|
|
11
|
+
*
|
|
12
|
+
* ## How
|
|
13
|
+
*
|
|
14
|
+
* 1. The module stores a deterministic secret in `runtimeConfig.nuxt-scripts.proxySecret`
|
|
15
|
+
* (env: `NUXT_SCRIPTS_PROXY_SECRET`).
|
|
16
|
+
* 2. URLs are canonicalized (sort query keys, strip `sig`) and signed with HMAC-SHA256.
|
|
17
|
+
* 3. The first 16 hex chars (64 bits) of the digest is appended as `?sig=<hex>`.
|
|
18
|
+
* 4. Endpoints wrapped with `withSigning()` verify the sig against the current request.
|
|
19
|
+
*
|
|
20
|
+
* A 64-bit signature is enough to defeat brute force for this threat model
|
|
21
|
+
* (a billion guesses gives a ~5% hit rate at 2^64). Longer signatures bloat
|
|
22
|
+
* prerendered HTML for no practical gain.
|
|
23
|
+
*/
|
|
24
|
+
import type { H3Event } from 'h3';
|
|
25
|
+
/** Query param name for the signature. Chosen to be unlikely to collide with upstream APIs. */
|
|
26
|
+
export declare const SIG_PARAM = "sig";
|
|
27
|
+
/** Length of the hex signature (16 chars = 64 bits). */
|
|
28
|
+
export declare const SIG_LENGTH = 16;
|
|
29
|
+
/**
|
|
30
|
+
* Canonicalize a query object into a deterministic string suitable for HMAC input.
|
|
31
|
+
*
|
|
32
|
+
* Rules:
|
|
33
|
+
* - The `sig` param is stripped (it can't sign itself).
|
|
34
|
+
* - `undefined` and `null` values are skipped (mirrors `ufo.withQuery`).
|
|
35
|
+
* - Keys are sorted alphabetically so order-independent reconstruction works.
|
|
36
|
+
* - Arrays expand to repeated keys (e.g. `markers=a&markers=b`), matching how
|
|
37
|
+
* `ufo.withQuery` serializes array-valued params.
|
|
38
|
+
* - Objects are JSON-stringified (rare, but consistent with `ufo.withQuery`).
|
|
39
|
+
* - Encoding uses `encodeURIComponent` for both keys and values so the canonical
|
|
40
|
+
* form matches what shows up on the wire.
|
|
41
|
+
*
|
|
42
|
+
* The resulting string is stable across server/client and different JS runtimes
|
|
43
|
+
* because it does not depend on `URLSearchParams` insertion order.
|
|
44
|
+
*/
|
|
45
|
+
export declare function canonicalizeQuery(query: Record<string, unknown>): string;
|
|
46
|
+
/**
|
|
47
|
+
* Sign a path + query using HMAC-SHA256 and return the 16-char hex digest.
|
|
48
|
+
*
|
|
49
|
+
* The HMAC input is `${path}?${canonicalQuery}` so that the same query signed
|
|
50
|
+
* against a different endpoint yields a different signature (prevents cross-
|
|
51
|
+
* endpoint signature reuse).
|
|
52
|
+
*
|
|
53
|
+
* `path` should be the URL path without query string (e.g. `/_scripts/proxy/google-static-maps`).
|
|
54
|
+
* Callers should not include origin / host since the signing contract is path-relative.
|
|
55
|
+
*/
|
|
56
|
+
export declare function signProxyUrl(path: string, query: Record<string, unknown>, secret: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Build a fully-formed signed URL (path + query + sig).
|
|
59
|
+
*
|
|
60
|
+
* This is the primary helper for code paths that need to emit a proxy URL
|
|
61
|
+
* (SSR components, server-side URL rewriters like instagram-embed).
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildSignedProxyUrl(path: string, query: Record<string, unknown>, secret: string): string;
|
|
64
|
+
/** Query param name for the page token. */
|
|
65
|
+
export declare const PAGE_TOKEN_PARAM = "_pt";
|
|
66
|
+
/** Query param name for the page token timestamp. */
|
|
67
|
+
export declare const PAGE_TOKEN_TS_PARAM = "_ts";
|
|
68
|
+
/** Default max age for page tokens in seconds (1 hour). */
|
|
69
|
+
export declare const PAGE_TOKEN_MAX_AGE = 3600;
|
|
70
|
+
/**
|
|
71
|
+
* Generate a page token that authorizes client-side proxy requests.
|
|
72
|
+
*
|
|
73
|
+
* Embedded in the SSR payload so the browser can attach it to reactive proxy
|
|
74
|
+
* URL updates without needing a `/sign` round-trip. The token is scoped to
|
|
75
|
+
* a timestamp and expires after `PAGE_TOKEN_MAX_AGE` seconds.
|
|
76
|
+
*
|
|
77
|
+
* Construction: first 16 hex chars of `HMAC(secret, "proxy-access:<timestamp>")`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function generateProxyToken(secret: string, timestamp: number): string;
|
|
80
|
+
/**
|
|
81
|
+
* Verify a page token against the current time.
|
|
82
|
+
*
|
|
83
|
+
* Returns `true` if the token matches the HMAC for the given timestamp AND
|
|
84
|
+
* the timestamp is within `maxAge` seconds of `now`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function verifyProxyToken(token: string, timestamp: number, secret: string, maxAge?: number, now?: number): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Verify a request against either a URL signature or a page token.
|
|
89
|
+
*
|
|
90
|
+
* Two verification modes, checked in order:
|
|
91
|
+
*
|
|
92
|
+
* 1. **URL signature** (`sig` param): the exact URL was signed server-side
|
|
93
|
+
* during SSR/prerender. Locked to the specific path + query params.
|
|
94
|
+
*
|
|
95
|
+
* 2. **Page token** (`_pt` + `_ts` params): the client received a short-lived
|
|
96
|
+
* token during SSR and is making a reactive proxy request with new params.
|
|
97
|
+
* Valid for any params on the target path, but expires after `maxAge`.
|
|
98
|
+
*
|
|
99
|
+
* Returns `false` if neither mode validates.
|
|
100
|
+
*/
|
|
101
|
+
export declare function verifyProxyRequest(event: H3Event, secret: string, maxAge?: number): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Constant-time string comparison.
|
|
104
|
+
*
|
|
105
|
+
* Both inputs are expected to be equal-length hex strings. The loop runs over
|
|
106
|
+
* the longer length so an early-exit on length mismatch doesn't leak the
|
|
107
|
+
* expected length (though both are fixed at `SIG_LENGTH` in practice).
|
|
108
|
+
*/
|
|
109
|
+
export declare function constantTimeEqual(a: string, b: string): boolean;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { getQuery } from "h3";
|
|
3
|
+
export const SIG_PARAM = "sig";
|
|
4
|
+
export const SIG_LENGTH = 16;
|
|
5
|
+
export function canonicalizeQuery(query) {
|
|
6
|
+
const keys = Object.keys(query).filter((k) => k !== SIG_PARAM && query[k] !== void 0 && query[k] !== null).sort();
|
|
7
|
+
const parts = [];
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
const value = query[key];
|
|
10
|
+
const encodedKey = encodeURIComponent(key);
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
for (const item of value) {
|
|
13
|
+
if (item === void 0 || item === null)
|
|
14
|
+
continue;
|
|
15
|
+
parts.push(`${encodedKey}=${encodeURIComponent(serializeValue(item))}`);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
parts.push(`${encodedKey}=${encodeURIComponent(serializeValue(value))}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return parts.join("&");
|
|
22
|
+
}
|
|
23
|
+
function serializeValue(value) {
|
|
24
|
+
if (typeof value === "string")
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === "object")
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
export function signProxyUrl(path, query, secret) {
|
|
31
|
+
const canonical = canonicalizeQuery(query);
|
|
32
|
+
const input = canonical ? `${path}?${canonical}` : path;
|
|
33
|
+
return createHmac("sha256", secret).update(input).digest("hex").slice(0, SIG_LENGTH);
|
|
34
|
+
}
|
|
35
|
+
export function buildSignedProxyUrl(path, query, secret) {
|
|
36
|
+
const sig = signProxyUrl(path, query, secret);
|
|
37
|
+
const canonical = canonicalizeQuery(query);
|
|
38
|
+
const queryString = canonical ? `${canonical}&${SIG_PARAM}=${sig}` : `${SIG_PARAM}=${sig}`;
|
|
39
|
+
return `${path}?${queryString}`;
|
|
40
|
+
}
|
|
41
|
+
export const PAGE_TOKEN_PARAM = "_pt";
|
|
42
|
+
export const PAGE_TOKEN_TS_PARAM = "_ts";
|
|
43
|
+
export const PAGE_TOKEN_MAX_AGE = 3600;
|
|
44
|
+
export function generateProxyToken(secret, timestamp) {
|
|
45
|
+
return createHmac("sha256", secret).update(`proxy-access:${timestamp}`).digest("hex").slice(0, SIG_LENGTH);
|
|
46
|
+
}
|
|
47
|
+
export function verifyProxyToken(token, timestamp, secret, maxAge = PAGE_TOKEN_MAX_AGE, now = Math.floor(Date.now() / 1e3)) {
|
|
48
|
+
if (!token || !secret || typeof timestamp !== "number")
|
|
49
|
+
return false;
|
|
50
|
+
if (token.length !== SIG_LENGTH)
|
|
51
|
+
return false;
|
|
52
|
+
const age = now - timestamp;
|
|
53
|
+
if (age > maxAge || age < -60)
|
|
54
|
+
return false;
|
|
55
|
+
const expected = generateProxyToken(secret, timestamp);
|
|
56
|
+
return constantTimeEqual(expected, token);
|
|
57
|
+
}
|
|
58
|
+
export function verifyProxyRequest(event, secret, maxAge) {
|
|
59
|
+
if (!secret)
|
|
60
|
+
return false;
|
|
61
|
+
const query = getQuery(event);
|
|
62
|
+
const rawSig = query[SIG_PARAM];
|
|
63
|
+
const sig = Array.isArray(rawSig) ? rawSig[0] : rawSig;
|
|
64
|
+
if (typeof sig === "string" && sig.length === SIG_LENGTH) {
|
|
65
|
+
const path = (event.path || "").split("?")[0] || "";
|
|
66
|
+
const expected = signProxyUrl(path, query, secret);
|
|
67
|
+
if (constantTimeEqual(expected, sig))
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const rawToken = query[PAGE_TOKEN_PARAM];
|
|
71
|
+
const rawTs = query[PAGE_TOKEN_TS_PARAM];
|
|
72
|
+
const token = Array.isArray(rawToken) ? rawToken[0] : rawToken;
|
|
73
|
+
const ts = Array.isArray(rawTs) ? rawTs[0] : rawTs;
|
|
74
|
+
if (typeof token === "string" && ts !== void 0) {
|
|
75
|
+
const timestamp = Number(ts);
|
|
76
|
+
if (!Number.isNaN(timestamp))
|
|
77
|
+
return verifyProxyToken(token, timestamp, secret, maxAge);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
export function constantTimeEqual(a, b) {
|
|
82
|
+
if (a.length !== b.length)
|
|
83
|
+
return false;
|
|
84
|
+
let diff = 0;
|
|
85
|
+
for (let i = 0; i < a.length; i++)
|
|
86
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
87
|
+
return diff === 0;
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware wrapper that enforces HMAC signature verification on a proxy handler.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```ts
|
|
6
|
+
* export default withSigning(defineEventHandler(async (event) => {
|
|
7
|
+
* // ... handler logic
|
|
8
|
+
* }))
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Behavior:
|
|
12
|
+
* - Reads `runtimeConfig.nuxt-scripts.proxySecret` (server-only).
|
|
13
|
+
* - If no secret is configured: passes through (signing not yet enabled).
|
|
14
|
+
* This allows shipping handler wiring before components emit signed URLs.
|
|
15
|
+
* Once `NUXT_SCRIPTS_PROXY_SECRET` is set, verification is enforced.
|
|
16
|
+
* - If a secret IS configured and the request's signature is invalid: 403.
|
|
17
|
+
* - Otherwise, delegates to the wrapped handler.
|
|
18
|
+
*
|
|
19
|
+
* The outer wrapper runs before any handler logic, so unauthorized requests
|
|
20
|
+
* never reach the upstream fetch and cannot consume API quota.
|
|
21
|
+
*/
|
|
22
|
+
import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3';
|
|
23
|
+
export declare function withSigning<Req extends EventHandlerRequest = EventHandlerRequest, Res extends EventHandlerResponse = EventHandlerResponse>(handler: EventHandler<Req, Res>): EventHandler<Req, Res>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createError, defineEventHandler } from "h3";
|
|
2
|
+
import { verifyProxyRequest } from "./sign.js";
|
|
3
|
+
export function withSigning(handler) {
|
|
4
|
+
return defineEventHandler(async (event) => {
|
|
5
|
+
const { useRuntimeConfig } = await import("#imports");
|
|
6
|
+
const runtimeConfig = useRuntimeConfig(event);
|
|
7
|
+
const secret = runtimeConfig["nuxt-scripts"]?.proxySecret;
|
|
8
|
+
if (!secret)
|
|
9
|
+
return handler(event);
|
|
10
|
+
if (!verifyProxyRequest(event, secret)) {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: 403,
|
|
13
|
+
statusMessage: "Invalid signature"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return handler(event);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
2
|
import { $fetch } from "ofetch";
|
|
3
|
+
import { withSigning } from "./utils/withSigning.js";
|
|
3
4
|
const TWEET_ID_RE = /^\d+$/;
|
|
4
|
-
export default defineEventHandler(async (event) => {
|
|
5
|
+
export default withSigning(defineEventHandler(async (event) => {
|
|
5
6
|
const query = getQuery(event);
|
|
6
7
|
const tweetId = query.id;
|
|
7
8
|
if (!tweetId || !TWEET_ID_RE.test(tweetId)) {
|
|
@@ -29,4 +30,4 @@ export default defineEventHandler(async (event) => {
|
|
|
29
30
|
setHeader(event, "Content-Type", "application/json");
|
|
30
31
|
setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
|
|
31
32
|
return tweetData;
|
|
32
|
-
});
|
|
33
|
+
}));
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -235,7 +235,7 @@ export type BuiltInRegistryScriptKey = 'bingUet' | 'blueskyEmbed' | 'carbonAds'
|
|
|
235
235
|
* Includes both built-in and augmented keys.
|
|
236
236
|
*/
|
|
237
237
|
export type RegistryScriptKey = Exclude<keyof ScriptRegistry, `${string}-npm`>;
|
|
238
|
-
type RegistryConfigInput<T> = [T] extends [true] ? Record<string, never> : T;
|
|
238
|
+
type RegistryConfigInput<T> = 0 extends 1 & T ? Record<string, any> : [T] extends [true] ? Record<string, never> : T;
|
|
239
239
|
export type NuxtConfigScriptRegistryEntry<T> = true | false | 'mock' | (RegistryConfigInput<T> & {
|
|
240
240
|
trigger?: NuxtUseScriptOptionsSerializable['trigger'] | false;
|
|
241
241
|
proxy?: boolean;
|
|
@@ -268,6 +268,14 @@ export interface RegistryScriptServerHandler {
|
|
|
268
268
|
route: string;
|
|
269
269
|
handler: string;
|
|
270
270
|
middleware?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Whether this handler verifies HMAC signatures via `withSigning()`.
|
|
273
|
+
*
|
|
274
|
+
* When any enabled script registers a handler with `requiresSigning: true`,
|
|
275
|
+
* the module enforces that `NUXT_SCRIPTS_PROXY_SECRET` is set in production,
|
|
276
|
+
* and the `/_scripts/sign` endpoint will accept this route as a signable path.
|
|
277
|
+
*/
|
|
278
|
+
requiresSigning?: boolean;
|
|
271
279
|
}
|
|
272
280
|
/**
|
|
273
281
|
* Declares what optimization modes a script supports and what's active by default.
|