@scalemule/news-publication 0.0.48 → 0.0.50
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 +54 -0
- package/dist/chunk-4JRWZXAP.js +62 -0
- package/dist/chunk-ABPJMDUG.js +251 -0
- package/dist/chunk-AFJ6S5WJ.js +124 -0
- package/dist/{chunk-GB36YM56.js → chunk-AMLVCZDP.js} +11 -1
- package/dist/chunk-QW3OZB22.js +616 -0
- package/dist/community-maps-view.d.cts +3 -3
- package/dist/community-maps-view.d.ts +3 -3
- package/dist/cookie-consent-banner.d.cts +2 -2
- package/dist/cookie-consent-banner.d.ts +2 -2
- package/dist/cookie-settings-view.d.cts +2 -2
- package/dist/cookie-settings-view.d.ts +2 -2
- package/dist/index.cjs +29 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/marketplace-forms.d.cts +3 -3
- package/dist/marketplace-forms.d.ts +3 -3
- package/dist/marketplace-view.d.cts +5 -5
- package/dist/marketplace-view.d.ts +5 -5
- package/dist/{meet-RWmfj3I3.d.cts → meet-BWpxY20X.d.ts} +5 -0
- package/dist/{meet-RWmfj3I3.d.ts → meet-CO9Unz5M.d.cts} +5 -0
- package/dist/meet-forms.d.cts +7 -6
- package/dist/meet-forms.d.ts +7 -6
- package/dist/meet-forms.js +2 -1
- package/dist/meet-view.cjs +22 -2
- package/dist/meet-view.d.cts +7 -5
- package/dist/meet-view.d.ts +7 -5
- package/dist/meet-view.js +21 -4
- package/dist/network-bar.d.cts +2 -2
- package/dist/network-bar.d.ts +2 -2
- package/dist/network-copyright.d.cts +2 -2
- package/dist/network-copyright.d.ts +2 -2
- package/dist/pets-auth-server.cjs +257 -0
- package/dist/pets-auth-server.d.cts +23 -0
- package/dist/pets-auth-server.d.ts +23 -0
- package/dist/pets-auth-server.js +198 -0
- package/dist/pets-forms.cjs +764 -0
- package/dist/pets-forms.d.cts +28 -0
- package/dist/pets-forms.d.ts +28 -0
- package/dist/pets-forms.js +714 -0
- package/dist/pets-interactions.cjs +333 -0
- package/dist/pets-interactions.d.cts +23 -0
- package/dist/pets-interactions.d.ts +23 -0
- package/dist/pets-interactions.js +320 -0
- package/dist/pets-plugin.cjs +546 -0
- package/dist/pets-plugin.d.cts +50 -0
- package/dist/pets-plugin.d.ts +50 -0
- package/dist/pets-plugin.js +312 -0
- package/dist/pets-publication.cjs +1014 -0
- package/dist/pets-publication.d.cts +40 -0
- package/dist/pets-publication.d.ts +40 -0
- package/dist/pets-publication.js +201 -0
- package/dist/pets-server.cjs +289 -0
- package/dist/pets-server.d.cts +42 -0
- package/dist/pets-server.d.ts +42 -0
- package/dist/pets-server.js +2 -0
- package/dist/pets-view.cjs +714 -0
- package/dist/pets-view.d.cts +58 -0
- package/dist/pets-view.d.ts +58 -0
- package/dist/pets-view.js +4 -0
- package/dist/pets.cjs +138 -0
- package/dist/pets.d.cts +127 -0
- package/dist/pets.d.ts +127 -0
- package/dist/pets.js +1 -0
- package/dist/sponsorship-view.d.cts +2 -2
- package/dist/sponsorship-view.d.ts +2 -2
- package/dist/sponsorship-view.js +2 -62
- package/dist/story-review.d.cts +6 -6
- package/dist/story-review.d.ts +6 -6
- package/dist/theme-toggle.d.cts +2 -2
- package/dist/theme-toggle.d.ts +2 -2
- package/dist/verified-card.d.cts +3 -3
- package/dist/verified-card.d.ts +3 -3
- package/package.json +45 -3
- package/pets.css +746 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var sdk = require('@scalemule/sdk');
|
|
5
|
+
|
|
6
|
+
// src/pets-auth-server.ts
|
|
7
|
+
|
|
8
|
+
// src/pets.ts
|
|
9
|
+
var PET_SLUG = /^[a-z0-9][a-z0-9-]{0,190}$/;
|
|
10
|
+
|
|
11
|
+
// src/pets-server.ts
|
|
12
|
+
var PetRequestError = class extends Error {
|
|
13
|
+
constructor(status, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
status;
|
|
18
|
+
};
|
|
19
|
+
var petPrivateHeaders = {
|
|
20
|
+
"cache-control": "private, no-store",
|
|
21
|
+
"x-robots-tag": "noindex, nofollow",
|
|
22
|
+
"referrer-policy": "no-referrer",
|
|
23
|
+
"x-content-type-options": "nosniff"
|
|
24
|
+
};
|
|
25
|
+
function config(scope) {
|
|
26
|
+
const url = new URL(scope.gatewayUrl);
|
|
27
|
+
if (!scope.apiKey || !PET_SLUG.test(scope.collectionSlug) || url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname)))
|
|
28
|
+
throw new PetRequestError(503, "The pet community is not available yet.");
|
|
29
|
+
return url.origin;
|
|
30
|
+
}
|
|
31
|
+
async function petRequest(scope, path, init = {}, fetcher = fetch) {
|
|
32
|
+
const gateway = config(scope);
|
|
33
|
+
if (!path.startsWith("/v1/") || path.includes("..") || /[\\\r\n]/.test(path))
|
|
34
|
+
throw new Error("Invalid platform path");
|
|
35
|
+
const response = await fetcher(gateway + path, {
|
|
36
|
+
method: init.method || "GET",
|
|
37
|
+
headers: {
|
|
38
|
+
...sdk.buildClientContextHeaders(scope.clientContext),
|
|
39
|
+
"x-api-key": scope.apiKey,
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
...scope.accessToken ? { authorization: `Bearer ${scope.accessToken}` } : {}
|
|
42
|
+
},
|
|
43
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body),
|
|
44
|
+
cache: "no-store",
|
|
45
|
+
redirect: "error",
|
|
46
|
+
signal: AbortSignal.timeout(15e3)
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok)
|
|
49
|
+
throw new PetRequestError(
|
|
50
|
+
response.status,
|
|
51
|
+
response.status === 401 ? "Please sign in to continue." : response.status === 404 ? "This pet resource is not available." : response.status === 409 ? "This contribution changed. Reload before saving." : response.status === 429 ? "Please wait before trying again." : "We couldn\u2019t complete that request. Please try again."
|
|
52
|
+
);
|
|
53
|
+
const json = await response.json();
|
|
54
|
+
return json?.data ?? json;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/pets-auth-server.ts
|
|
58
|
+
var PAW_SESSION_COOKIE = "__Host-paw-reader";
|
|
59
|
+
var PAW_FLOW_COOKIE = "__Host-paw-signin";
|
|
60
|
+
var PAW_SIGNED_OUT_COOKIE = "__Host-paw-signed-out";
|
|
61
|
+
var FLOW = PAW_FLOW_COOKIE;
|
|
62
|
+
var attrs = "Path=/; HttpOnly; Secure; SameSite=Lax";
|
|
63
|
+
function pawCookie(request, name = PAW_SESSION_COOKIE) {
|
|
64
|
+
const values = (request.headers.get("cookie") || "").split(";").map((v) => v.trim()).filter((v) => v.startsWith(name + "="));
|
|
65
|
+
return values.length === 1 ? values[0].slice(name.length + 1) : void 0;
|
|
66
|
+
}
|
|
67
|
+
function digest(value) {
|
|
68
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
function returnPath(value) {
|
|
71
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && value.length <= 500 && !/[\\\r\n]/.test(value) ? value : "/account";
|
|
72
|
+
}
|
|
73
|
+
function sealPawFlow(state, next, key, now = Date.now()) {
|
|
74
|
+
const payload = Buffer.from(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
state: digest(state),
|
|
77
|
+
next: returnPath(next),
|
|
78
|
+
expires: now + 6e5
|
|
79
|
+
})
|
|
80
|
+
).toString("base64url");
|
|
81
|
+
return payload + "." + crypto.createHmac("sha256", key).update("paw-signin:v1:" + payload).digest("hex");
|
|
82
|
+
}
|
|
83
|
+
function openPawFlow(raw, state, key, now = Date.now()) {
|
|
84
|
+
if (!key || !raw || raw.length > 2048 || !state || state.length > 2048)
|
|
85
|
+
return null;
|
|
86
|
+
const [payload, signature, extra] = raw.split(".");
|
|
87
|
+
if (!payload || extra || !/^[a-f0-9]{64}$/.test(signature || "")) return null;
|
|
88
|
+
if (!crypto.timingSafeEqual(
|
|
89
|
+
Buffer.from(signature, "hex"),
|
|
90
|
+
crypto.createHmac("sha256", key).update("paw-signin:v1:" + payload).digest()
|
|
91
|
+
))
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
const flow = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
95
|
+
if (!Number.isFinite(flow.expires) || flow.expires <= now || !/^[a-f0-9]{64}$/.test(flow.state) || !crypto.timingSafeEqual(
|
|
96
|
+
Buffer.from(flow.state, "hex"),
|
|
97
|
+
Buffer.from(digest(state), "hex")
|
|
98
|
+
))
|
|
99
|
+
return null;
|
|
100
|
+
return returnPath(flow.next);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function createPawAuthHandler(options) {
|
|
106
|
+
return async (request, action) => {
|
|
107
|
+
const json = (body, status = 200) => Response.json(body, { status, headers: petPrivateHeaders });
|
|
108
|
+
try {
|
|
109
|
+
if (!options.enabled)
|
|
110
|
+
return json({ error: "Community sign-in is not available yet." }, 503);
|
|
111
|
+
const scope = await options.resolve(request), origin = new URL(scope.origin).origin;
|
|
112
|
+
if (!origin.startsWith("https://") || !scope.apiKey)
|
|
113
|
+
return json({ error: "Sign-in is not configured." }, 503);
|
|
114
|
+
const account = returnPath(options.accountPath || "/account");
|
|
115
|
+
const sessionCookie = options.sessionCookie || PAW_SESSION_COOKIE;
|
|
116
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(sessionCookie))
|
|
117
|
+
throw new Error("Invalid cookie name");
|
|
118
|
+
const retryPath = (path, reason) => {
|
|
119
|
+
const target = new URL(returnPath(path), origin);
|
|
120
|
+
target.searchParams.set("signin", reason);
|
|
121
|
+
return target.pathname + target.search;
|
|
122
|
+
};
|
|
123
|
+
const redirect = (path) => new Response(null, {
|
|
124
|
+
status: 303,
|
|
125
|
+
headers: {
|
|
126
|
+
...petPrivateHeaders,
|
|
127
|
+
location: origin + returnPath(path)
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
const resuming = action === "resume" && request.method === "GET";
|
|
131
|
+
if (resuming || action === "start" && request.method === "POST") {
|
|
132
|
+
if (!resuming && request.headers.get("origin") !== origin || resuming && (request.headers.has("sec-fetch-mode") && request.headers.get("sec-fetch-mode") !== "navigate" || request.headers.has("sec-fetch-dest") && request.headers.get("sec-fetch-dest") !== "document"))
|
|
133
|
+
return json({ error: "Sign in from this website." }, 403);
|
|
134
|
+
const next = returnPath(
|
|
135
|
+
new URL(request.url).searchParams.get("next") || account
|
|
136
|
+
);
|
|
137
|
+
if (resuming && pawCookie(request, PAW_SIGNED_OUT_COOKIE))
|
|
138
|
+
return redirect(retryPath(next, "required"));
|
|
139
|
+
const callback = origin + returnPath(options.callbackPath || "/api/paw-auth/callback");
|
|
140
|
+
const result = await petRequest(
|
|
141
|
+
scope,
|
|
142
|
+
"/v1/auth/oauth/ledvery/authorize?" + new URLSearchParams({
|
|
143
|
+
redirect_uri: callback,
|
|
144
|
+
...resuming ? { prompt: "none" } : {}
|
|
145
|
+
}),
|
|
146
|
+
{},
|
|
147
|
+
options.fetch
|
|
148
|
+
);
|
|
149
|
+
const url = new URL(result.url);
|
|
150
|
+
if (url.protocol !== "https:" || url.origin !== new URL(options.issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.searchParams.get("state") !== result.state || url.searchParams.get("redirect_uri") !== callback || url.searchParams.get("code_challenge_method") !== "S256" || !url.searchParams.get("code_challenge"))
|
|
151
|
+
throw new Error("Invalid authorization response");
|
|
152
|
+
const response = resuming ? new Response(null, {
|
|
153
|
+
status: 303,
|
|
154
|
+
headers: { ...petPrivateHeaders, location: url.href }
|
|
155
|
+
}) : json({ url: url.href });
|
|
156
|
+
if (!resuming)
|
|
157
|
+
response.headers.append(
|
|
158
|
+
"set-cookie",
|
|
159
|
+
`${PAW_SIGNED_OUT_COOKIE}=; ${attrs}; Max-Age=0`
|
|
160
|
+
);
|
|
161
|
+
response.headers.append(
|
|
162
|
+
"set-cookie",
|
|
163
|
+
`${FLOW}=${sealPawFlow(result.state, next, scope.apiKey)}; ${attrs}; Max-Age=600`
|
|
164
|
+
);
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
if (action === "callback" && request.method === "GET") {
|
|
168
|
+
const query = new URL(request.url).searchParams, code = query.get("code"), state = query.get("state");
|
|
169
|
+
const next = openPawFlow(pawCookie(request, FLOW), state, scope.apiKey);
|
|
170
|
+
const response = redirect(next || retryPath(account, "retry"));
|
|
171
|
+
response.headers.append("set-cookie", `${FLOW}=; ${attrs}; Max-Age=0`);
|
|
172
|
+
if (!next || !code || code.length > 4096 || query.has("error")) {
|
|
173
|
+
response.headers.set(
|
|
174
|
+
"location",
|
|
175
|
+
origin + retryPath(
|
|
176
|
+
next || account,
|
|
177
|
+
["login_required", "consent_required"].includes(
|
|
178
|
+
query.get("error") || ""
|
|
179
|
+
) ? "required" : "retry"
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
return response;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const session = await petRequest(
|
|
186
|
+
scope,
|
|
187
|
+
"/v1/auth/oauth/ledvery/callback?" + new URLSearchParams({ code, state }),
|
|
188
|
+
{},
|
|
189
|
+
options.fetch
|
|
190
|
+
);
|
|
191
|
+
const expiry = Date.parse(session.expires_at);
|
|
192
|
+
if (!session.session_token || !/^[A-Za-z0-9._~-]{1,2048}$/.test(session.session_token) || !Number.isFinite(expiry) || expiry <= Date.now())
|
|
193
|
+
throw new Error("Invalid session");
|
|
194
|
+
const me = await petRequest(
|
|
195
|
+
{ ...scope, accessToken: session.session_token },
|
|
196
|
+
"/v1/auth/me",
|
|
197
|
+
{},
|
|
198
|
+
options.fetch
|
|
199
|
+
);
|
|
200
|
+
if (!me.id || !me.email_verified)
|
|
201
|
+
throw new Error("Verified reader required");
|
|
202
|
+
response.headers.append(
|
|
203
|
+
"set-cookie",
|
|
204
|
+
`${sessionCookie}=${session.session_token}; ${attrs}; Expires=${new Date(expiry).toUTCString()}`
|
|
205
|
+
);
|
|
206
|
+
return response;
|
|
207
|
+
} catch {
|
|
208
|
+
response.headers.set("location", origin + retryPath(next, "retry"));
|
|
209
|
+
return response;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (action === "session" && request.method === "GET") {
|
|
213
|
+
if (!scope.accessToken) return json({ authenticated: false });
|
|
214
|
+
const me = await petRequest(scope, "/v1/auth/me", {}, options.fetch);
|
|
215
|
+
return json({
|
|
216
|
+
authenticated: Boolean(me.id && me.email_verified),
|
|
217
|
+
name: me.full_name || "Neighbor"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (action === "logout" && request.method === "POST") {
|
|
221
|
+
if (request.headers.get("origin") !== origin)
|
|
222
|
+
return json({ error: "Continue from this website." }, 403);
|
|
223
|
+
if (scope.accessToken)
|
|
224
|
+
await petRequest(
|
|
225
|
+
scope,
|
|
226
|
+
"/v1/auth/logout",
|
|
227
|
+
{ method: "POST", body: { session_token: scope.accessToken } },
|
|
228
|
+
options.fetch
|
|
229
|
+
);
|
|
230
|
+
const response = json({ authenticated: false });
|
|
231
|
+
response.headers.append(
|
|
232
|
+
"set-cookie",
|
|
233
|
+
`${sessionCookie}=; ${attrs}; Max-Age=0`
|
|
234
|
+
);
|
|
235
|
+
response.headers.append(
|
|
236
|
+
"set-cookie",
|
|
237
|
+
`${PAW_SIGNED_OUT_COOKIE}=1; ${attrs}; Max-Age=2592000`
|
|
238
|
+
);
|
|
239
|
+
return response;
|
|
240
|
+
}
|
|
241
|
+
return json({ error: "Route not found" }, 404);
|
|
242
|
+
} catch {
|
|
243
|
+
return json(
|
|
244
|
+
{ error: "Sign-in is temporarily unavailable. Please try again." },
|
|
245
|
+
503
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
exports.PAW_FLOW_COOKIE = PAW_FLOW_COOKIE;
|
|
252
|
+
exports.PAW_SESSION_COOKIE = PAW_SESSION_COOKIE;
|
|
253
|
+
exports.PAW_SIGNED_OUT_COOKIE = PAW_SIGNED_OUT_COOKIE;
|
|
254
|
+
exports.createPawAuthHandler = createPawAuthHandler;
|
|
255
|
+
exports.openPawFlow = openPawFlow;
|
|
256
|
+
exports.pawCookie = pawCookie;
|
|
257
|
+
exports.sealPawFlow = sealPawFlow;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PetScope } from './pets-server.cjs';
|
|
2
|
+
import '@scalemule/sdk';
|
|
3
|
+
import './sponsorships-CtnBe3S8.cjs';
|
|
4
|
+
import './pets.cjs';
|
|
5
|
+
|
|
6
|
+
declare const PAW_SESSION_COOKIE = "__Host-paw-reader";
|
|
7
|
+
declare const PAW_FLOW_COOKIE = "__Host-paw-signin";
|
|
8
|
+
declare const PAW_SIGNED_OUT_COOKIE = "__Host-paw-signed-out";
|
|
9
|
+
declare function pawCookie(request: Request, name?: string): string | undefined;
|
|
10
|
+
declare function sealPawFlow(state: string, next: string, key: string, now?: number): string;
|
|
11
|
+
declare function openPawFlow(raw: string | undefined, state: string | null, key: string, now?: number): string | null;
|
|
12
|
+
declare function createPawAuthHandler(options: {
|
|
13
|
+
resolve(request: Request): Promise<PetScope>;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
issuerOrigin: string;
|
|
16
|
+
/** Reuse the publication’s existing host-only reader cookie and registered callback. */
|
|
17
|
+
sessionCookie?: string;
|
|
18
|
+
callbackPath?: string;
|
|
19
|
+
accountPath?: string;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
}): (request: Request, action: string) => Promise<Response>;
|
|
22
|
+
|
|
23
|
+
export { PAW_FLOW_COOKIE, PAW_SESSION_COOKIE, PAW_SIGNED_OUT_COOKIE, createPawAuthHandler, openPawFlow, pawCookie, sealPawFlow };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PetScope } from './pets-server.js';
|
|
2
|
+
import '@scalemule/sdk';
|
|
3
|
+
import './sponsorships-CtnBe3S8.js';
|
|
4
|
+
import './pets.js';
|
|
5
|
+
|
|
6
|
+
declare const PAW_SESSION_COOKIE = "__Host-paw-reader";
|
|
7
|
+
declare const PAW_FLOW_COOKIE = "__Host-paw-signin";
|
|
8
|
+
declare const PAW_SIGNED_OUT_COOKIE = "__Host-paw-signed-out";
|
|
9
|
+
declare function pawCookie(request: Request, name?: string): string | undefined;
|
|
10
|
+
declare function sealPawFlow(state: string, next: string, key: string, now?: number): string;
|
|
11
|
+
declare function openPawFlow(raw: string | undefined, state: string | null, key: string, now?: number): string | null;
|
|
12
|
+
declare function createPawAuthHandler(options: {
|
|
13
|
+
resolve(request: Request): Promise<PetScope>;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
issuerOrigin: string;
|
|
16
|
+
/** Reuse the publication’s existing host-only reader cookie and registered callback. */
|
|
17
|
+
sessionCookie?: string;
|
|
18
|
+
callbackPath?: string;
|
|
19
|
+
accountPath?: string;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
}): (request: Request, action: string) => Promise<Response>;
|
|
22
|
+
|
|
23
|
+
export { PAW_FLOW_COOKIE, PAW_SESSION_COOKIE, PAW_SIGNED_OUT_COOKIE, createPawAuthHandler, openPawFlow, pawCookie, sealPawFlow };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { petPrivateHeaders, petRequest } from './chunk-ABPJMDUG.js';
|
|
2
|
+
import './chunk-AFJ6S5WJ.js';
|
|
3
|
+
import { createHmac, timingSafeEqual, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
var PAW_SESSION_COOKIE = "__Host-paw-reader";
|
|
6
|
+
var PAW_FLOW_COOKIE = "__Host-paw-signin";
|
|
7
|
+
var PAW_SIGNED_OUT_COOKIE = "__Host-paw-signed-out";
|
|
8
|
+
var FLOW = PAW_FLOW_COOKIE;
|
|
9
|
+
var attrs = "Path=/; HttpOnly; Secure; SameSite=Lax";
|
|
10
|
+
function pawCookie(request, name = PAW_SESSION_COOKIE) {
|
|
11
|
+
const values = (request.headers.get("cookie") || "").split(";").map((v) => v.trim()).filter((v) => v.startsWith(name + "="));
|
|
12
|
+
return values.length === 1 ? values[0].slice(name.length + 1) : void 0;
|
|
13
|
+
}
|
|
14
|
+
function digest(value) {
|
|
15
|
+
return createHash("sha256").update(value).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
function returnPath(value) {
|
|
18
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && value.length <= 500 && !/[\\\r\n]/.test(value) ? value : "/account";
|
|
19
|
+
}
|
|
20
|
+
function sealPawFlow(state, next, key, now = Date.now()) {
|
|
21
|
+
const payload = Buffer.from(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
state: digest(state),
|
|
24
|
+
next: returnPath(next),
|
|
25
|
+
expires: now + 6e5
|
|
26
|
+
})
|
|
27
|
+
).toString("base64url");
|
|
28
|
+
return payload + "." + createHmac("sha256", key).update("paw-signin:v1:" + payload).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function openPawFlow(raw, state, key, now = Date.now()) {
|
|
31
|
+
if (!key || !raw || raw.length > 2048 || !state || state.length > 2048)
|
|
32
|
+
return null;
|
|
33
|
+
const [payload, signature, extra] = raw.split(".");
|
|
34
|
+
if (!payload || extra || !/^[a-f0-9]{64}$/.test(signature || "")) return null;
|
|
35
|
+
if (!timingSafeEqual(
|
|
36
|
+
Buffer.from(signature, "hex"),
|
|
37
|
+
createHmac("sha256", key).update("paw-signin:v1:" + payload).digest()
|
|
38
|
+
))
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const flow = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
42
|
+
if (!Number.isFinite(flow.expires) || flow.expires <= now || !/^[a-f0-9]{64}$/.test(flow.state) || !timingSafeEqual(
|
|
43
|
+
Buffer.from(flow.state, "hex"),
|
|
44
|
+
Buffer.from(digest(state), "hex")
|
|
45
|
+
))
|
|
46
|
+
return null;
|
|
47
|
+
return returnPath(flow.next);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function createPawAuthHandler(options) {
|
|
53
|
+
return async (request, action) => {
|
|
54
|
+
const json = (body, status = 200) => Response.json(body, { status, headers: petPrivateHeaders });
|
|
55
|
+
try {
|
|
56
|
+
if (!options.enabled)
|
|
57
|
+
return json({ error: "Community sign-in is not available yet." }, 503);
|
|
58
|
+
const scope = await options.resolve(request), origin = new URL(scope.origin).origin;
|
|
59
|
+
if (!origin.startsWith("https://") || !scope.apiKey)
|
|
60
|
+
return json({ error: "Sign-in is not configured." }, 503);
|
|
61
|
+
const account = returnPath(options.accountPath || "/account");
|
|
62
|
+
const sessionCookie = options.sessionCookie || PAW_SESSION_COOKIE;
|
|
63
|
+
if (!/^[A-Za-z0-9_-]{1,100}$/.test(sessionCookie))
|
|
64
|
+
throw new Error("Invalid cookie name");
|
|
65
|
+
const retryPath = (path, reason) => {
|
|
66
|
+
const target = new URL(returnPath(path), origin);
|
|
67
|
+
target.searchParams.set("signin", reason);
|
|
68
|
+
return target.pathname + target.search;
|
|
69
|
+
};
|
|
70
|
+
const redirect = (path) => new Response(null, {
|
|
71
|
+
status: 303,
|
|
72
|
+
headers: {
|
|
73
|
+
...petPrivateHeaders,
|
|
74
|
+
location: origin + returnPath(path)
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const resuming = action === "resume" && request.method === "GET";
|
|
78
|
+
if (resuming || action === "start" && request.method === "POST") {
|
|
79
|
+
if (!resuming && request.headers.get("origin") !== origin || resuming && (request.headers.has("sec-fetch-mode") && request.headers.get("sec-fetch-mode") !== "navigate" || request.headers.has("sec-fetch-dest") && request.headers.get("sec-fetch-dest") !== "document"))
|
|
80
|
+
return json({ error: "Sign in from this website." }, 403);
|
|
81
|
+
const next = returnPath(
|
|
82
|
+
new URL(request.url).searchParams.get("next") || account
|
|
83
|
+
);
|
|
84
|
+
if (resuming && pawCookie(request, PAW_SIGNED_OUT_COOKIE))
|
|
85
|
+
return redirect(retryPath(next, "required"));
|
|
86
|
+
const callback = origin + returnPath(options.callbackPath || "/api/paw-auth/callback");
|
|
87
|
+
const result = await petRequest(
|
|
88
|
+
scope,
|
|
89
|
+
"/v1/auth/oauth/ledvery/authorize?" + new URLSearchParams({
|
|
90
|
+
redirect_uri: callback,
|
|
91
|
+
...resuming ? { prompt: "none" } : {}
|
|
92
|
+
}),
|
|
93
|
+
{},
|
|
94
|
+
options.fetch
|
|
95
|
+
);
|
|
96
|
+
const url = new URL(result.url);
|
|
97
|
+
if (url.protocol !== "https:" || url.origin !== new URL(options.issuerOrigin).origin || url.pathname !== "/authorize" || url.username || url.password || url.searchParams.get("state") !== result.state || url.searchParams.get("redirect_uri") !== callback || url.searchParams.get("code_challenge_method") !== "S256" || !url.searchParams.get("code_challenge"))
|
|
98
|
+
throw new Error("Invalid authorization response");
|
|
99
|
+
const response = resuming ? new Response(null, {
|
|
100
|
+
status: 303,
|
|
101
|
+
headers: { ...petPrivateHeaders, location: url.href }
|
|
102
|
+
}) : json({ url: url.href });
|
|
103
|
+
if (!resuming)
|
|
104
|
+
response.headers.append(
|
|
105
|
+
"set-cookie",
|
|
106
|
+
`${PAW_SIGNED_OUT_COOKIE}=; ${attrs}; Max-Age=0`
|
|
107
|
+
);
|
|
108
|
+
response.headers.append(
|
|
109
|
+
"set-cookie",
|
|
110
|
+
`${FLOW}=${sealPawFlow(result.state, next, scope.apiKey)}; ${attrs}; Max-Age=600`
|
|
111
|
+
);
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
if (action === "callback" && request.method === "GET") {
|
|
115
|
+
const query = new URL(request.url).searchParams, code = query.get("code"), state = query.get("state");
|
|
116
|
+
const next = openPawFlow(pawCookie(request, FLOW), state, scope.apiKey);
|
|
117
|
+
const response = redirect(next || retryPath(account, "retry"));
|
|
118
|
+
response.headers.append("set-cookie", `${FLOW}=; ${attrs}; Max-Age=0`);
|
|
119
|
+
if (!next || !code || code.length > 4096 || query.has("error")) {
|
|
120
|
+
response.headers.set(
|
|
121
|
+
"location",
|
|
122
|
+
origin + retryPath(
|
|
123
|
+
next || account,
|
|
124
|
+
["login_required", "consent_required"].includes(
|
|
125
|
+
query.get("error") || ""
|
|
126
|
+
) ? "required" : "retry"
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const session = await petRequest(
|
|
133
|
+
scope,
|
|
134
|
+
"/v1/auth/oauth/ledvery/callback?" + new URLSearchParams({ code, state }),
|
|
135
|
+
{},
|
|
136
|
+
options.fetch
|
|
137
|
+
);
|
|
138
|
+
const expiry = Date.parse(session.expires_at);
|
|
139
|
+
if (!session.session_token || !/^[A-Za-z0-9._~-]{1,2048}$/.test(session.session_token) || !Number.isFinite(expiry) || expiry <= Date.now())
|
|
140
|
+
throw new Error("Invalid session");
|
|
141
|
+
const me = await petRequest(
|
|
142
|
+
{ ...scope, accessToken: session.session_token },
|
|
143
|
+
"/v1/auth/me",
|
|
144
|
+
{},
|
|
145
|
+
options.fetch
|
|
146
|
+
);
|
|
147
|
+
if (!me.id || !me.email_verified)
|
|
148
|
+
throw new Error("Verified reader required");
|
|
149
|
+
response.headers.append(
|
|
150
|
+
"set-cookie",
|
|
151
|
+
`${sessionCookie}=${session.session_token}; ${attrs}; Expires=${new Date(expiry).toUTCString()}`
|
|
152
|
+
);
|
|
153
|
+
return response;
|
|
154
|
+
} catch {
|
|
155
|
+
response.headers.set("location", origin + retryPath(next, "retry"));
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (action === "session" && request.method === "GET") {
|
|
160
|
+
if (!scope.accessToken) return json({ authenticated: false });
|
|
161
|
+
const me = await petRequest(scope, "/v1/auth/me", {}, options.fetch);
|
|
162
|
+
return json({
|
|
163
|
+
authenticated: Boolean(me.id && me.email_verified),
|
|
164
|
+
name: me.full_name || "Neighbor"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (action === "logout" && request.method === "POST") {
|
|
168
|
+
if (request.headers.get("origin") !== origin)
|
|
169
|
+
return json({ error: "Continue from this website." }, 403);
|
|
170
|
+
if (scope.accessToken)
|
|
171
|
+
await petRequest(
|
|
172
|
+
scope,
|
|
173
|
+
"/v1/auth/logout",
|
|
174
|
+
{ method: "POST", body: { session_token: scope.accessToken } },
|
|
175
|
+
options.fetch
|
|
176
|
+
);
|
|
177
|
+
const response = json({ authenticated: false });
|
|
178
|
+
response.headers.append(
|
|
179
|
+
"set-cookie",
|
|
180
|
+
`${sessionCookie}=; ${attrs}; Max-Age=0`
|
|
181
|
+
);
|
|
182
|
+
response.headers.append(
|
|
183
|
+
"set-cookie",
|
|
184
|
+
`${PAW_SIGNED_OUT_COOKIE}=1; ${attrs}; Max-Age=2592000`
|
|
185
|
+
);
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
return json({ error: "Route not found" }, 404);
|
|
189
|
+
} catch {
|
|
190
|
+
return json(
|
|
191
|
+
{ error: "Sign-in is temporarily unavailable. Please try again." },
|
|
192
|
+
503
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { PAW_FLOW_COOKIE, PAW_SESSION_COOKIE, PAW_SIGNED_OUT_COOKIE, createPawAuthHandler, openPawFlow, pawCookie, sealPawFlow };
|