@redeemid/emdash 0.0.15 → 0.0.17
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 +416 -223
- package/docs/shopify-setup.md +1 -1
- package/package.json +2 -1
- package/src/access-admin.ts +754 -0
- package/src/access.ts +84 -18
- package/src/admin-api.ts +9 -2
- package/src/agents.ts +7 -3
- package/src/astro-modules.d.ts +4 -0
- package/src/components/AccessBadge.astro +7 -5
- package/src/components/Account.astro +1 -1
- package/src/components/Cart.astro +1 -1
- package/src/components/CartView.astro +1 -1
- package/src/components/Collection.astro +1 -1
- package/src/components/Content.astro +7 -3
- package/src/components/Contribution.astro +1 -1
- package/src/components/Gate.astro +4 -2
- package/src/components/LockedPanel.astro +1 -1
- package/src/components/MemberCard.astro +1 -1
- package/src/components/Notices.astro +2 -1
- package/src/components/Plan.astro +1 -1
- package/src/components/Preferences.astro +1 -1
- package/src/components/Product.astro +1 -1
- package/src/components/YourData.astro +1 -1
- package/src/config.ts +17 -5
- package/src/descriptor.ts +7 -0
- package/src/events.ts +3 -0
- package/src/features.ts +1 -1
- package/src/files.ts +1 -1
- package/src/guard.ts +5 -3
- package/src/index.ts +4 -1
- package/src/log.ts +96 -0
- package/src/monitor.ts +560 -0
- package/src/plugin.ts +202 -86
- package/src/rate-limit-cloudflare.ts +20 -0
- package/src/rate-limit.ts +66 -0
- package/src/routes/callback.ts +21 -4
- package/src/routes/preferences.ts +2 -2
- package/src/routes/privacy.ts +2 -2
- package/src/routes/webhook.ts +24 -11
- package/src/settings.ts +195 -0
- package/src/site-config.ts +38 -5
- package/src/staff.ts +12 -23
- package/src/storage.ts +12 -0
- package/src/text.ts +1 -0
- package/src/version.ts +1 -1
- package/src/webhooks.ts +2 -1
package/src/files.ts
CHANGED
|
@@ -101,7 +101,7 @@ async function readsWhole(astro: AstroLike, entry: { collection: string; id: str
|
|
|
101
101
|
const m = (await viewer(astro)).member;
|
|
102
102
|
if (m && m.status !== "disabled" && holdsItem(item, m.tags)) return true;
|
|
103
103
|
}
|
|
104
|
-
const verdict = await readEntry(astro, { collection: entry.collection, terms });
|
|
104
|
+
const verdict = await readEntry(astro, { collection: entry.collection, terms, ruleId: entry.id });
|
|
105
105
|
if (!verdict.allowed) return false;
|
|
106
106
|
const clauses = bodyClauses(body);
|
|
107
107
|
return clauses.every((clause) => clause.some((l) => verdict.viewer.levels.includes(l) || (l === "signed-in" && verdict.viewer.signedIn)));
|
package/src/guard.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { signInUrl } from "./access.js";
|
|
|
25
25
|
import { readCart } from "./cart.js";
|
|
26
26
|
import { keysFromPath, pathAccess, refusal } from "./files.js";
|
|
27
27
|
import { AFTER_LOGOUT_COOKIE, readAfterLogout } from "./routes/shared.js";
|
|
28
|
-
import { siteConfig } from "./site-config.js";
|
|
28
|
+
import { shareConfig, siteConfig } from "./site-config.js";
|
|
29
29
|
|
|
30
30
|
const SUBSCRIBER = 10;
|
|
31
31
|
|
|
@@ -55,7 +55,9 @@ async function withUser(context: { locals: unknown; session?: { get(k: string):
|
|
|
55
55
|
const { createKyselyAdapter } = await import("@emdash-cms/auth/adapters/kysely");
|
|
56
56
|
const user = await createKyselyAdapter(locals.emdash.db as never).getUserById(sessionUser.id);
|
|
57
57
|
if (!user || user.disabled) return context;
|
|
58
|
-
|
|
58
|
+
const next = { ...locals, user: { id: user.id, role: user.role, disabled: user.disabled } };
|
|
59
|
+
shareConfig(locals, next);
|
|
60
|
+
return { ...context, locals: next };
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/** Keep one visitor's response out of every shared cache, the route cache included. */
|
|
@@ -75,7 +77,7 @@ export function keepPrivate(context: { cache?: { set(options: false): void } },
|
|
|
75
77
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
76
78
|
const { locals, url } = context;
|
|
77
79
|
const user = (locals as { user?: { role?: number } | null }).user;
|
|
78
|
-
const config = siteConfig(
|
|
80
|
+
const config = await siteConfig(locals);
|
|
79
81
|
|
|
80
82
|
// Protected files: checked for everyone, on every download.
|
|
81
83
|
const keys = config?.files?.length ? keysFromPath(url.pathname) : null;
|
package/src/index.ts
CHANGED
|
@@ -70,11 +70,14 @@ export function redeemid(config: RedeemIDConfig): AuthProviderDescriptor {
|
|
|
70
70
|
* Astro integration that keeps members (EmDash Subscribers) out of EmDash's
|
|
71
71
|
* content API and admin, so only RedeemID's checks decide who reads what.
|
|
72
72
|
*/
|
|
73
|
-
export function redeemidGuard(): AstroIntegration {
|
|
73
|
+
export function redeemidGuard(options: { rateLimit?: boolean } = {}): AstroIntegration {
|
|
74
74
|
return {
|
|
75
75
|
name: "@redeemid/emdash/guard",
|
|
76
76
|
hooks: {
|
|
77
77
|
"astro:config:setup": ({ addMiddleware }) => {
|
|
78
|
+
// Cloudflare Workers only: limits Sign in, Buy and cart changes per
|
|
79
|
+
// visitor with the REDEEMID_RATE_LIMIT Rate Limiting binding.
|
|
80
|
+
if (options.rateLimit) addMiddleware({ entrypoint: "@redeemid/emdash/rate-limit-cloudflare.ts", order: "pre" });
|
|
78
81
|
addMiddleware({ entrypoint: "@redeemid/emdash/guard.ts", order: "post" });
|
|
79
82
|
},
|
|
80
83
|
},
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App events: what RedeemID did and what came of it, for the admin's App
|
|
3
|
+
* events page. Webhooks received and rejected, sign-ins and refusals, calls to
|
|
4
|
+
* Shopify's Admin API, staff actions, settings saves and outbound events.
|
|
5
|
+
*
|
|
6
|
+
* Each entry holds ids, labels, outcomes and timings only, never request or
|
|
7
|
+
* response bodies, tokens or emails. Kept 30 days.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StorageCollection } from "emdash";
|
|
11
|
+
|
|
12
|
+
export const LOG_DAYS = 30;
|
|
13
|
+
|
|
14
|
+
export type LogOrigin = "webhook" | "member" | "staff" | "agent" | "cron" | "site";
|
|
15
|
+
|
|
16
|
+
export interface LogEntry {
|
|
17
|
+
at: string;
|
|
18
|
+
/** Dotted name, e.g. "webhook.received", "shopify.admin". */
|
|
19
|
+
event: string;
|
|
20
|
+
/** What it was about, e.g. a webhook topic or an Admin API operation. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** ok, or what went wrong, e.g. "processed", "rejected", "error". */
|
|
23
|
+
status: string;
|
|
24
|
+
/** "ok" | "warn" | "error", for colour and the Problems filter. */
|
|
25
|
+
level: "ok" | "warn" | "error";
|
|
26
|
+
origin?: LogOrigin;
|
|
27
|
+
customerId?: string | null;
|
|
28
|
+
ms?: number;
|
|
29
|
+
/** A short note, never a body or a secret. */
|
|
30
|
+
note?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Event names, for the filter. */
|
|
34
|
+
export const LOG_EVENTS: Record<string, string> = {
|
|
35
|
+
"webhook.received": "Webhook received",
|
|
36
|
+
"webhook.rejected": "Webhook rejected",
|
|
37
|
+
"webhook.failed": "Webhook failed",
|
|
38
|
+
"webhook.quiet": "No webhooks lately",
|
|
39
|
+
"member.signed_in": "Sign-in",
|
|
40
|
+
"member.refused": "Sign-in refused",
|
|
41
|
+
"shopify.admin": "Admin API call",
|
|
42
|
+
"staff.resync": "Resync",
|
|
43
|
+
"staff.tags": "Tag edit",
|
|
44
|
+
"staff.webhooks": "Webhook registration",
|
|
45
|
+
"settings.saved": "Settings saved",
|
|
46
|
+
"events.failed": "Outbound event failed",
|
|
47
|
+
"health.check": "Health check",
|
|
48
|
+
"meter.used": "Metered read",
|
|
49
|
+
"meter.exhausted": "Metered read, meter used up",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type Sink = (entry: LogEntry) => Promise<void>;
|
|
53
|
+
const SINK = Symbol.for("redeemid:log-sink");
|
|
54
|
+
|
|
55
|
+
/** Where entries go (the plugin's storage, or a test's array). */
|
|
56
|
+
export function setLogSink(sink: Sink | null): void {
|
|
57
|
+
(globalThis as unknown as Record<symbol, Sink | null>)[SINK] = sink;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function defaultSink(entry: LogEntry): Promise<void> {
|
|
61
|
+
const [{ getDb }, { stores }] = await Promise.all([import("emdash/runtime"), import("./storage.js")]);
|
|
62
|
+
const s = stores(await getDb());
|
|
63
|
+
await s.log.put(`${entry.at}:${crypto.randomUUID().slice(0, 8)}`, entry);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Record one entry. Never throws: a log that can't be written never breaks the work. */
|
|
67
|
+
export async function record(entry: Omit<LogEntry, "at" | "level"> & { level?: LogEntry["level"] }): Promise<void> {
|
|
68
|
+
const full: LogEntry = { at: new Date().toISOString(), level: entry.level ?? "ok", ...entry } as LogEntry;
|
|
69
|
+
try {
|
|
70
|
+
const sink = (globalThis as unknown as Record<symbol, Sink | null | undefined>)[SINK] ?? defaultSink;
|
|
71
|
+
await sink(full);
|
|
72
|
+
} catch {
|
|
73
|
+
// No storage here (e.g. outside a request): skip.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Time a call and record it. */
|
|
78
|
+
export async function timed<T>(event: string, label: string, run: () => Promise<T>, extra: Partial<LogEntry> = {}): Promise<T> {
|
|
79
|
+
const started = Date.now();
|
|
80
|
+
try {
|
|
81
|
+
const out = await run();
|
|
82
|
+
await record({ event, label, status: "ok", ms: Date.now() - started, ...extra });
|
|
83
|
+
return out;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
await record({ event, label, status: "error", level: "error", ms: Date.now() - started, note: String((e as Error).message ?? e).slice(0, 200), ...extra });
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Delete entries (and other dated records) older than `days`, a page at a time. */
|
|
91
|
+
export async function prune<T>(store: StorageCollection<T>, field: string, days = LOG_DAYS): Promise<number> {
|
|
92
|
+
const before = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
93
|
+
const { items } = await store.query({ where: { [field]: { lt: before } }, limit: 100 });
|
|
94
|
+
if (items.length === 0) return 0;
|
|
95
|
+
return store.deleteMany(items.map((i) => i.id));
|
|
96
|
+
}
|