@odla-ai/chapter 0.34.0 → 0.35.0
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 +83 -2
- package/dist/index.cjs +124 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +124 -3
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +143 -4
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +99 -1
- package/dist/worker/index.d.ts +99 -1
- package/dist/worker/index.js +143 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -209,6 +209,56 @@ interface ChapterPolicy {
|
|
|
209
209
|
* (join, Stripe membership, booking, member area, admin, CRM); `hub` is
|
|
210
210
|
* admin-only and CRM-focused (a directory/registry over the same CRM). */
|
|
211
211
|
type ChapterMode = "chapter" | "hub";
|
|
212
|
+
/** One odla-db transaction op — the wire shape both the real admin client and a
|
|
213
|
+
* test double (`FakeDb`) accept. Built as a plain object, never via a `tx` proxy. */
|
|
214
|
+
type DbOp<Id extends string | {
|
|
215
|
+
ns: string;
|
|
216
|
+
attr: string;
|
|
217
|
+
value: string | number | boolean;
|
|
218
|
+
} = string> = {
|
|
219
|
+
t: "update" | "merge";
|
|
220
|
+
ns: string;
|
|
221
|
+
id: Id;
|
|
222
|
+
attrs: Record<string, unknown>;
|
|
223
|
+
} | {
|
|
224
|
+
t: "retract";
|
|
225
|
+
ns: string;
|
|
226
|
+
id: Id;
|
|
227
|
+
attrs: string[];
|
|
228
|
+
} | {
|
|
229
|
+
t: "delete";
|
|
230
|
+
ns: string;
|
|
231
|
+
id: Id;
|
|
232
|
+
attrs?: undefined;
|
|
233
|
+
};
|
|
234
|
+
/** The structural view of the odla-db admin client the worker builds and the
|
|
235
|
+
* member routes consume — so route logic is testable against an in-memory fake. */
|
|
236
|
+
interface ChapterDb {
|
|
237
|
+
query(q: Record<string, unknown>): Promise<Record<string, Array<Record<string, unknown>>>>;
|
|
238
|
+
/** Optional bounded aggregate supported by the real privileged odla-db
|
|
239
|
+
* client. Routes use it to prove a result set is non-empty before issuing
|
|
240
|
+
* an ordered lookup, while small test doubles may continue to omit it. */
|
|
241
|
+
aggregate?(ns: string, spec: {
|
|
242
|
+
count?: boolean;
|
|
243
|
+
}, opts?: {
|
|
244
|
+
where?: Record<string, unknown>;
|
|
245
|
+
}): Promise<{
|
|
246
|
+
count?: number;
|
|
247
|
+
}>;
|
|
248
|
+
transact(ops: DbOp<string | {
|
|
249
|
+
ns: string;
|
|
250
|
+
attr: string;
|
|
251
|
+
value: string | number | boolean;
|
|
252
|
+
}>[], opts?: {
|
|
253
|
+
mutationId?: string;
|
|
254
|
+
}): Promise<{
|
|
255
|
+
txId: number;
|
|
256
|
+
duplicate: boolean;
|
|
257
|
+
}>;
|
|
258
|
+
secrets: {
|
|
259
|
+
get(name: string): Promise<string>;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
212
262
|
/** The scalar kinds an odla-db attribute can hold. */
|
|
213
263
|
type AttrType = "string" | "number" | "boolean" | "json";
|
|
214
264
|
/** One odla-db attribute: its type and index/uniqueness/optionality flags. */
|
|
@@ -790,6 +840,54 @@ interface SignupControlDeliveryOptions {
|
|
|
790
840
|
/** Build and send one signed desired-state delivery. Durable retry belongs to the hub outbox. */
|
|
791
841
|
declare function deliverSignupControl(options: SignupControlDeliveryOptions): Promise<SignupControlReceipt>;
|
|
792
842
|
|
|
843
|
+
/** Identity shared by initial HTML, response caching, and client hydration. */
|
|
844
|
+
interface SignupControlThemeMetadata {
|
|
845
|
+
source: "active" | "bootstrap";
|
|
846
|
+
revision: number | null;
|
|
847
|
+
digest: string | null;
|
|
848
|
+
/** Include this value in any host-owned cache key for transformed HTML. */
|
|
849
|
+
cacheKey: string;
|
|
850
|
+
}
|
|
851
|
+
/** A validated style block ready to place in an HTML head. */
|
|
852
|
+
interface SignupControlThemeHead {
|
|
853
|
+
css: string;
|
|
854
|
+
head: string;
|
|
855
|
+
metadata: SignupControlThemeMetadata;
|
|
856
|
+
}
|
|
857
|
+
/** Host-owned fallback inputs shared by server rendering and hydration. */
|
|
858
|
+
interface SignupControlThemeOptions {
|
|
859
|
+
/** Local site-owned tokens used only when no active managed revision exists. */
|
|
860
|
+
bootstrapTokens?: Readonly<Record<string, string | number>>;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Inputs required to resolve one follower-local active signup theme. */
|
|
864
|
+
interface ActiveSignupControlThemeOptions extends SignupControlThemeOptions {
|
|
865
|
+
/** Follower-local database client. No controlling-hub request is performed. */
|
|
866
|
+
db: ChapterDb;
|
|
867
|
+
chapterId: string;
|
|
868
|
+
environment: string;
|
|
869
|
+
/** Optional host marker; defaults to `SIGNUP_CONTROL_THEME_MARKER`. */
|
|
870
|
+
marker?: string;
|
|
871
|
+
}
|
|
872
|
+
/** Transformed HTML plus the exact theme and cache identity it contains. */
|
|
873
|
+
interface ActiveSignupControlHtml {
|
|
874
|
+
html: string;
|
|
875
|
+
theme: SignupControlThemeHead;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Transform trusted host HTML with the follower-local last-known-good signup
|
|
879
|
+
* theme. A missing active revision uses the documented bootstrap state; a read
|
|
880
|
+
* failure or corrupt active row fails closed instead of fetching the hub.
|
|
881
|
+
*/
|
|
882
|
+
declare function renderActiveSignupControlHtml(html: string, options: ActiveSignupControlThemeOptions): Promise<ActiveSignupControlHtml>;
|
|
883
|
+
/**
|
|
884
|
+
* Transform an HTML response and make the cache contract safe by default.
|
|
885
|
+
* `no-store` prevents a prior digest from surviving activation; hosts that own
|
|
886
|
+
* a cache may instead key it with `result.theme.metadata.cacheKey` from
|
|
887
|
+
* {@link renderActiveSignupControlHtml}.
|
|
888
|
+
*/
|
|
889
|
+
declare function withActiveSignupControlTheme(response: Response, options: ActiveSignupControlThemeOptions): Promise<Response>;
|
|
890
|
+
|
|
793
891
|
/**
|
|
794
892
|
* Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT
|
|
795
893
|
* verification, the source-aware admin gate, the mounted @odla-ai/crm routes, the
|
|
@@ -812,4 +910,4 @@ declare function chapterWorker(options: ChapterWorkerOptions): {
|
|
|
812
910
|
fetch(req: Request, env: ChapterEnv): Promise<Response>;
|
|
813
911
|
};
|
|
814
912
|
|
|
815
|
-
export { type ChapterEnv, type ChapterWorkerOptions, type Route, type SignupControlDeliveryOptions, type WorkerContext, chapterWorker, createWorkerContext, deliverSignupControl };
|
|
913
|
+
export { type ActiveSignupControlHtml, type ActiveSignupControlThemeOptions, type ChapterEnv, type ChapterWorkerOptions, type Route, type SignupControlDeliveryOptions, type WorkerContext, chapterWorker, createWorkerContext, deliverSignupControl, renderActiveSignupControlHtml, withActiveSignupControlTheme };
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -209,6 +209,56 @@ interface ChapterPolicy {
|
|
|
209
209
|
* (join, Stripe membership, booking, member area, admin, CRM); `hub` is
|
|
210
210
|
* admin-only and CRM-focused (a directory/registry over the same CRM). */
|
|
211
211
|
type ChapterMode = "chapter" | "hub";
|
|
212
|
+
/** One odla-db transaction op — the wire shape both the real admin client and a
|
|
213
|
+
* test double (`FakeDb`) accept. Built as a plain object, never via a `tx` proxy. */
|
|
214
|
+
type DbOp<Id extends string | {
|
|
215
|
+
ns: string;
|
|
216
|
+
attr: string;
|
|
217
|
+
value: string | number | boolean;
|
|
218
|
+
} = string> = {
|
|
219
|
+
t: "update" | "merge";
|
|
220
|
+
ns: string;
|
|
221
|
+
id: Id;
|
|
222
|
+
attrs: Record<string, unknown>;
|
|
223
|
+
} | {
|
|
224
|
+
t: "retract";
|
|
225
|
+
ns: string;
|
|
226
|
+
id: Id;
|
|
227
|
+
attrs: string[];
|
|
228
|
+
} | {
|
|
229
|
+
t: "delete";
|
|
230
|
+
ns: string;
|
|
231
|
+
id: Id;
|
|
232
|
+
attrs?: undefined;
|
|
233
|
+
};
|
|
234
|
+
/** The structural view of the odla-db admin client the worker builds and the
|
|
235
|
+
* member routes consume — so route logic is testable against an in-memory fake. */
|
|
236
|
+
interface ChapterDb {
|
|
237
|
+
query(q: Record<string, unknown>): Promise<Record<string, Array<Record<string, unknown>>>>;
|
|
238
|
+
/** Optional bounded aggregate supported by the real privileged odla-db
|
|
239
|
+
* client. Routes use it to prove a result set is non-empty before issuing
|
|
240
|
+
* an ordered lookup, while small test doubles may continue to omit it. */
|
|
241
|
+
aggregate?(ns: string, spec: {
|
|
242
|
+
count?: boolean;
|
|
243
|
+
}, opts?: {
|
|
244
|
+
where?: Record<string, unknown>;
|
|
245
|
+
}): Promise<{
|
|
246
|
+
count?: number;
|
|
247
|
+
}>;
|
|
248
|
+
transact(ops: DbOp<string | {
|
|
249
|
+
ns: string;
|
|
250
|
+
attr: string;
|
|
251
|
+
value: string | number | boolean;
|
|
252
|
+
}>[], opts?: {
|
|
253
|
+
mutationId?: string;
|
|
254
|
+
}): Promise<{
|
|
255
|
+
txId: number;
|
|
256
|
+
duplicate: boolean;
|
|
257
|
+
}>;
|
|
258
|
+
secrets: {
|
|
259
|
+
get(name: string): Promise<string>;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
212
262
|
/** The scalar kinds an odla-db attribute can hold. */
|
|
213
263
|
type AttrType = "string" | "number" | "boolean" | "json";
|
|
214
264
|
/** One odla-db attribute: its type and index/uniqueness/optionality flags. */
|
|
@@ -790,6 +840,54 @@ interface SignupControlDeliveryOptions {
|
|
|
790
840
|
/** Build and send one signed desired-state delivery. Durable retry belongs to the hub outbox. */
|
|
791
841
|
declare function deliverSignupControl(options: SignupControlDeliveryOptions): Promise<SignupControlReceipt>;
|
|
792
842
|
|
|
843
|
+
/** Identity shared by initial HTML, response caching, and client hydration. */
|
|
844
|
+
interface SignupControlThemeMetadata {
|
|
845
|
+
source: "active" | "bootstrap";
|
|
846
|
+
revision: number | null;
|
|
847
|
+
digest: string | null;
|
|
848
|
+
/** Include this value in any host-owned cache key for transformed HTML. */
|
|
849
|
+
cacheKey: string;
|
|
850
|
+
}
|
|
851
|
+
/** A validated style block ready to place in an HTML head. */
|
|
852
|
+
interface SignupControlThemeHead {
|
|
853
|
+
css: string;
|
|
854
|
+
head: string;
|
|
855
|
+
metadata: SignupControlThemeMetadata;
|
|
856
|
+
}
|
|
857
|
+
/** Host-owned fallback inputs shared by server rendering and hydration. */
|
|
858
|
+
interface SignupControlThemeOptions {
|
|
859
|
+
/** Local site-owned tokens used only when no active managed revision exists. */
|
|
860
|
+
bootstrapTokens?: Readonly<Record<string, string | number>>;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Inputs required to resolve one follower-local active signup theme. */
|
|
864
|
+
interface ActiveSignupControlThemeOptions extends SignupControlThemeOptions {
|
|
865
|
+
/** Follower-local database client. No controlling-hub request is performed. */
|
|
866
|
+
db: ChapterDb;
|
|
867
|
+
chapterId: string;
|
|
868
|
+
environment: string;
|
|
869
|
+
/** Optional host marker; defaults to `SIGNUP_CONTROL_THEME_MARKER`. */
|
|
870
|
+
marker?: string;
|
|
871
|
+
}
|
|
872
|
+
/** Transformed HTML plus the exact theme and cache identity it contains. */
|
|
873
|
+
interface ActiveSignupControlHtml {
|
|
874
|
+
html: string;
|
|
875
|
+
theme: SignupControlThemeHead;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Transform trusted host HTML with the follower-local last-known-good signup
|
|
879
|
+
* theme. A missing active revision uses the documented bootstrap state; a read
|
|
880
|
+
* failure or corrupt active row fails closed instead of fetching the hub.
|
|
881
|
+
*/
|
|
882
|
+
declare function renderActiveSignupControlHtml(html: string, options: ActiveSignupControlThemeOptions): Promise<ActiveSignupControlHtml>;
|
|
883
|
+
/**
|
|
884
|
+
* Transform an HTML response and make the cache contract safe by default.
|
|
885
|
+
* `no-store` prevents a prior digest from surviving activation; hosts that own
|
|
886
|
+
* a cache may instead key it with `result.theme.metadata.cacheKey` from
|
|
887
|
+
* {@link renderActiveSignupControlHtml}.
|
|
888
|
+
*/
|
|
889
|
+
declare function withActiveSignupControlTheme(response: Response, options: ActiveSignupControlThemeOptions): Promise<Response>;
|
|
890
|
+
|
|
793
891
|
/**
|
|
794
892
|
* Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT
|
|
795
893
|
* verification, the source-aware admin gate, the mounted @odla-ai/crm routes, the
|
|
@@ -812,4 +910,4 @@ declare function chapterWorker(options: ChapterWorkerOptions): {
|
|
|
812
910
|
fetch(req: Request, env: ChapterEnv): Promise<Response>;
|
|
813
911
|
};
|
|
814
912
|
|
|
815
|
-
export { type ChapterEnv, type ChapterWorkerOptions, type Route, type SignupControlDeliveryOptions, type WorkerContext, chapterWorker, createWorkerContext, deliverSignupControl };
|
|
913
|
+
export { type ActiveSignupControlHtml, type ActiveSignupControlThemeOptions, type ChapterEnv, type ChapterWorkerOptions, type Route, type SignupControlDeliveryOptions, type WorkerContext, chapterWorker, createWorkerContext, deliverSignupControl, renderActiveSignupControlHtml, withActiveSignupControlTheme };
|
package/dist/worker/index.js
CHANGED
|
@@ -1727,12 +1727,18 @@ function themeOf(value, errors) {
|
|
|
1727
1727
|
}
|
|
1728
1728
|
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
1729
1729
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
1730
|
+
let serializedLength = 0;
|
|
1730
1731
|
for (const [key, token] of Object.entries(tokens)) {
|
|
1731
|
-
if (!/^--[a-z0-9-]{
|
|
1732
|
-
|
|
1733
|
-
|
|
1732
|
+
if (!/^--ui-[a-z0-9][a-z0-9-]{0,94}$/.test(key)) {
|
|
1733
|
+
errors.push(`document.theme.tokens.${key} must be an allowlisted --ui-* custom property`);
|
|
1734
|
+
}
|
|
1735
|
+
const rendered = String(token).trim();
|
|
1736
|
+
serializedLength += key.length + rendered.length + 4;
|
|
1737
|
+
if (typeof token !== "string" && typeof token !== "number" || typeof token === "number" && !Number.isFinite(token) || !rendered || rendered.length > 1e3 || /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i.test(rendered)) {
|
|
1738
|
+
errors.push(`document.theme.tokens.${key} must be a bounded breakout-safe CSS value`);
|
|
1734
1739
|
}
|
|
1735
1740
|
}
|
|
1741
|
+
if (serializedLength > 65536) errors.push("document.theme.tokens exceeds the serialized size bound");
|
|
1736
1742
|
return { tokens };
|
|
1737
1743
|
}
|
|
1738
1744
|
function validateSignupControlDocument(input) {
|
|
@@ -4058,6 +4064,137 @@ async function deliverSignupControl(options) {
|
|
|
4058
4064
|
return result;
|
|
4059
4065
|
}
|
|
4060
4066
|
|
|
4067
|
+
// src/signup-control-theme.ts
|
|
4068
|
+
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
4069
|
+
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
4070
|
+
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
4071
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
4072
|
+
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
4073
|
+
var MAX_TOKENS = 256;
|
|
4074
|
+
var MAX_VALUE_LENGTH = 1e3;
|
|
4075
|
+
var MAX_SERIALIZED_LENGTH = 65536;
|
|
4076
|
+
function validatedTokens(input) {
|
|
4077
|
+
const entries = Object.entries(input ?? {});
|
|
4078
|
+
if (entries.length > MAX_TOKENS) {
|
|
4079
|
+
throw new TypeError(`signup-control theme supports at most ${MAX_TOKENS} tokens`);
|
|
4080
|
+
}
|
|
4081
|
+
const tokens = {};
|
|
4082
|
+
let serializedLength = 0;
|
|
4083
|
+
for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
4084
|
+
if (!TOKEN_NAME.test(name)) {
|
|
4085
|
+
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
4086
|
+
}
|
|
4087
|
+
if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
|
|
4088
|
+
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
4089
|
+
}
|
|
4090
|
+
const value = String(raw).trim();
|
|
4091
|
+
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
4092
|
+
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
4093
|
+
}
|
|
4094
|
+
serializedLength += name.length + value.length + 4;
|
|
4095
|
+
if (serializedLength > MAX_SERIALIZED_LENGTH) {
|
|
4096
|
+
throw new TypeError("signup-control theme exceeds the serialized size bound");
|
|
4097
|
+
}
|
|
4098
|
+
tokens[name] = value;
|
|
4099
|
+
}
|
|
4100
|
+
return tokens;
|
|
4101
|
+
}
|
|
4102
|
+
function cssFor(tokens) {
|
|
4103
|
+
const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
|
|
4104
|
+
return declarations.length ? `:root {
|
|
4105
|
+
${declarations.join("\n")}
|
|
4106
|
+
}` : "";
|
|
4107
|
+
}
|
|
4108
|
+
function stateFor(revision, options) {
|
|
4109
|
+
if (!revision) {
|
|
4110
|
+
return {
|
|
4111
|
+
css: cssFor(validatedTokens(options.bootstrapTokens)),
|
|
4112
|
+
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
|
|
4116
|
+
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
4117
|
+
}
|
|
4118
|
+
return {
|
|
4119
|
+
css: cssFor(validatedTokens(revision.theme?.tokens)),
|
|
4120
|
+
metadata: {
|
|
4121
|
+
source: "active",
|
|
4122
|
+
revision: revision.revision,
|
|
4123
|
+
digest: revision.digest,
|
|
4124
|
+
cacheKey: `signup-control:${revision.digest}`
|
|
4125
|
+
}
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
function attributes(metadata) {
|
|
4129
|
+
return [
|
|
4130
|
+
`id="${SIGNUP_CONTROL_THEME_STYLE_ID}"`,
|
|
4131
|
+
`data-signup-control-source="${metadata.source}"`,
|
|
4132
|
+
`data-signup-control-cache-key="${metadata.cacheKey}"`,
|
|
4133
|
+
...metadata.revision === null ? [] : [`data-signup-control-revision="${metadata.revision}"`],
|
|
4134
|
+
...metadata.digest === null ? [] : [`data-signup-control-digest="${metadata.digest}"`]
|
|
4135
|
+
].join(" ");
|
|
4136
|
+
}
|
|
4137
|
+
function renderSignupControlThemeHead(revision, options = {}) {
|
|
4138
|
+
const { css, metadata } = stateFor(revision, options);
|
|
4139
|
+
return { css, metadata, head: `<style ${attributes(metadata)}>${css}</style>` };
|
|
4140
|
+
}
|
|
4141
|
+
function injectSignupControlThemeHead(html, rendered, marker = SIGNUP_CONTROL_THEME_MARKER) {
|
|
4142
|
+
const existing = /<style\b(?=[^>]*\bid\s*=\s*["']odla-signup-control-theme["'])[^>]*>[\s\S]*?<\/style>/gi;
|
|
4143
|
+
const existingBlocks = html.match(existing) ?? [];
|
|
4144
|
+
if (existingBlocks.length > 1) {
|
|
4145
|
+
throw new TypeError("signup-control theme style block must appear at most once");
|
|
4146
|
+
}
|
|
4147
|
+
if (existingBlocks.length === 1) return html.replace(existing, rendered.head);
|
|
4148
|
+
const markerIndex = html.indexOf(marker);
|
|
4149
|
+
if (markerIndex >= 0) {
|
|
4150
|
+
if (html.indexOf(marker, markerIndex + marker.length) >= 0) {
|
|
4151
|
+
throw new TypeError("signup-control theme marker must appear exactly once");
|
|
4152
|
+
}
|
|
4153
|
+
return `${html.slice(0, markerIndex)}${rendered.head}${html.slice(markerIndex + marker.length)}`;
|
|
4154
|
+
}
|
|
4155
|
+
const closingHead = html.search(/<\/head\s*>/i);
|
|
4156
|
+
if (closingHead < 0) throw new TypeError("signup-control theme requires an HTML head");
|
|
4157
|
+
return `${html.slice(0, closingHead)}${rendered.head}${html.slice(closingHead)}`;
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
// src/signup-control-theme-worker.ts
|
|
4161
|
+
async function activeTheme(options) {
|
|
4162
|
+
const active = await loadActiveSignupControl(options.db, options.chapterId, options.environment);
|
|
4163
|
+
if (!active) return null;
|
|
4164
|
+
const checked = validateSignupControlDocument(active.document);
|
|
4165
|
+
if (!checked.ok) throw new TypeError(`active signup-control document is invalid: ${checked.errors.join("; ")}`);
|
|
4166
|
+
const document = checked.value;
|
|
4167
|
+
if (document.revision !== active.revision || document.target.chapterId !== options.chapterId || document.target.environment !== options.environment || active.digest !== await signupControlDigest(document)) {
|
|
4168
|
+
throw new TypeError("active signup-control revision metadata or digest is invalid");
|
|
4169
|
+
}
|
|
4170
|
+
return { revision: active.revision, digest: active.digest, theme: document.theme };
|
|
4171
|
+
}
|
|
4172
|
+
async function renderActiveSignupControlHtml(html, options) {
|
|
4173
|
+
const theme = renderSignupControlThemeHead(await activeTheme(options), {
|
|
4174
|
+
bootstrapTokens: options.bootstrapTokens
|
|
4175
|
+
});
|
|
4176
|
+
return { html: injectSignupControlThemeHead(html, theme, options.marker), theme };
|
|
4177
|
+
}
|
|
4178
|
+
async function contentEtag(html) {
|
|
4179
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(html));
|
|
4180
|
+
const hex = Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4181
|
+
return `W/"sha256-${hex}"`;
|
|
4182
|
+
}
|
|
4183
|
+
async function withActiveSignupControlTheme(response, options) {
|
|
4184
|
+
if (!response.ok || !(response.headers.get("content-type") ?? "").toLowerCase().includes("text/html")) {
|
|
4185
|
+
throw new TypeError("signup-control theme can only transform a successful HTML response");
|
|
4186
|
+
}
|
|
4187
|
+
const rendered = await renderActiveSignupControlHtml(await response.text(), options);
|
|
4188
|
+
const headers = new Headers(response.headers);
|
|
4189
|
+
headers.delete("content-length");
|
|
4190
|
+
headers.set("cache-control", "no-store");
|
|
4191
|
+
headers.set("etag", await contentEtag(rendered.html));
|
|
4192
|
+
headers.set("x-odla-signup-control-digest", rendered.theme.metadata.digest ?? "bootstrap");
|
|
4193
|
+
if (rendered.theme.metadata.revision === null) headers.delete("x-odla-signup-control-revision");
|
|
4194
|
+
else headers.set("x-odla-signup-control-revision", String(rendered.theme.metadata.revision));
|
|
4195
|
+
return new Response(rendered.html, { status: response.status, statusText: response.statusText, headers });
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4061
4198
|
// src/worker.ts
|
|
4062
4199
|
var BUILTIN_ROUTES = [
|
|
4063
4200
|
handleHealth,
|
|
@@ -4124,6 +4261,8 @@ function chapterWorker(options) {
|
|
|
4124
4261
|
export {
|
|
4125
4262
|
chapterWorker,
|
|
4126
4263
|
createWorkerContext,
|
|
4127
|
-
deliverSignupControl
|
|
4264
|
+
deliverSignupControl,
|
|
4265
|
+
renderActiveSignupControlHtml,
|
|
4266
|
+
withActiveSignupControlTheme
|
|
4128
4267
|
};
|
|
4129
4268
|
//# sourceMappingURL=index.js.map
|