@authhero/cloudflare-adapter 3.0.12 → 3.1.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.
@@ -0,0 +1,61 @@
1
+ import { AnalyticsEngineDataset } from "../analytics-engine-logs/types";
2
+ /**
3
+ * One metric emission from the authhero outbox relay.
4
+ *
5
+ * Structurally identical to authhero's `OutboxMetric`. It is redeclared here
6
+ * so this adapter keeps working without `authhero` installed — it is only an
7
+ * optional peer dependency of this package.
8
+ */
9
+ export interface OutboxMetricRecord {
10
+ /**
11
+ * `outbox_events_processed_total`, `outbox_events_dead_lettered_total` or
12
+ * `outbox_retry_delay_seconds`.
13
+ */
14
+ name: string;
15
+ /** Counter increment, or the observed retry delay in seconds. */
16
+ value: number;
17
+ tenantId: string;
18
+ eventType: string;
19
+ source: "request" | "cron";
20
+ destination?: string;
21
+ error?: string;
22
+ retryCount?: number;
23
+ }
24
+ export interface AnalyticsEngineOutboxMetricsConfig {
25
+ /**
26
+ * Cloudflare Analytics Engine dataset binding (e.g. `env.OUTBOX_METRICS`).
27
+ * When absent the sink is a no-op, so the same wiring works locally.
28
+ */
29
+ analyticsEngineBinding?: AnalyticsEngineDataset;
30
+ }
31
+ /**
32
+ * Create an Analytics Engine sink for outbox relay metrics.
33
+ *
34
+ * Pass the returned function to `init({ outbox: { metrics } })` and to
35
+ * `runOutboxRelay({ metrics })` so both the inline per-request relay and the
36
+ * cron drain report to the same dataset. Rows are indexed by tenant, matching
37
+ * `createAnalyticsEngineLogsAdapter`.
38
+ *
39
+ * Column layout:
40
+ * - blob1 `name`, blob2 `tenant_id`, blob3 `event_type`, blob4 `source`,
41
+ * blob5 `destination`, blob6 `error`
42
+ * - double1 `value`, double2 `retry_count`, double3 `timestamp` (ms)
43
+ * - index1 `tenant_id`
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // wrangler.toml:
48
+ * // [[analytics_engine_datasets]]
49
+ * // binding = "OUTBOX_METRICS"
50
+ * // dataset = "authhero_outbox_metrics"
51
+ *
52
+ * import { createAnalyticsEngineOutboxMetricsSink } from "@authhero/cloudflare-adapter";
53
+ *
54
+ * const metrics = createAnalyticsEngineOutboxMetricsSink({
55
+ * analyticsEngineBinding: env.OUTBOX_METRICS,
56
+ * });
57
+ *
58
+ * const app = init({ dataAdapter, outbox: { enabled: true, metrics } });
59
+ * ```
60
+ */
61
+ export declare function createAnalyticsEngineOutboxMetricsSink(config: AnalyticsEngineOutboxMetricsConfig): (metric: OutboxMetricRecord) => void;
@@ -1,3 +1,26 @@
1
- import { CustomDomainsAdapter } from "@authhero/adapter-interfaces";
1
+ import { CustomDomain, CustomDomainsAdapter } from "@authhero/adapter-interfaces";
2
2
  import { CloudflareConfig } from "../types/CloudflareConfig";
3
+ import { CustomDomainResult } from "../types/CustomDomain";
4
+ /** @internal Shared with the `syncCustomDomains` sweep. */
5
+ export declare function getClient(config: CloudflareConfig): import("wretch").Wretch<unknown, unknown, undefined, undefined, never>;
6
+ /**
7
+ * Merge a fresh Cloudflare hostname result onto the stored row and mirror the
8
+ * result back to the DB, so `list()` and `getByDomain()` — which never call
9
+ * Cloudflare — converge on it.
10
+ *
11
+ * Shared by `get()` and `syncCustomDomains()` so the interactive refresh and
12
+ * the cron sweep can never drift apart on what a Cloudflare payload means.
13
+ *
14
+ * Never throws — both callers are refresh paths, and neither should fail
15
+ * because Cloudflare returned something surprising. Failure is reported in
16
+ * `outcome` rather than raised, and is kept distinct from `"unchanged"`: a
17
+ * sweep whose writebacks are all failing (a varchar overflow on `verification`
18
+ * is one we have actually seen) would otherwise be indistinguishable from a
19
+ * sweep that found nothing to do.
20
+ */
21
+ export type RefreshOutcome = "unchanged" | "updated" | "failed";
22
+ export declare function refreshFromCloudflare(config: CloudflareConfig, tenant_id: string, stored: CustomDomain, result: CustomDomainResult): Promise<{
23
+ domain: CustomDomain;
24
+ outcome: RefreshOutcome;
25
+ }>;
3
26
  export declare function createCustomDomainsAdapter(config: CloudflareConfig): CustomDomainsAdapter;
@@ -0,0 +1,78 @@
1
+ import { CloudflareConfig } from "../types/CloudflareConfig";
2
+ export interface SyncCustomDomainsOptions {
3
+ /**
4
+ * Hostnames fetched per Cloudflare API call. Cloudflare accepts 5 to 1000
5
+ * (its own default is 20); values outside that are clamped rather than
6
+ * rejected, since a cron should not die on a config typo. Defaults to 50.
7
+ */
8
+ perPage?: number;
9
+ /**
10
+ * Safety stop, in pages. A zone larger than `perPage * maxPages` is swept
11
+ * partially rather than looping forever on a paginating API that never
12
+ * reports a short page. Default 200 (10 000 hostnames at the default size).
13
+ */
14
+ maxPages?: number;
15
+ }
16
+ export interface SyncCustomDomainsResult {
17
+ /** Hostnames returned by Cloudflare across every page. */
18
+ scanned: number;
19
+ /** Hostnames that matched a stored custom-domain row. */
20
+ matched: number;
21
+ /** Rows whose `status` or `verification` actually changed. */
22
+ updated: number;
23
+ /**
24
+ * Hostnames in the zone with no stored row. Expected to be 0 in a zone
25
+ * AuthHero owns exclusively; a non-zero count means either a hostname
26
+ * registered outside AuthHero or a `create` that died between the Cloudflare
27
+ * call and the DB write.
28
+ */
29
+ unknown: number;
30
+ /**
31
+ * Stored rows whose `custom_domain_id` no longer matches the hostname's id
32
+ * at the edge — the hostname was deleted and re-registered behind our back.
33
+ * Left untouched: adopting the new id would rewrite a primary key that
34
+ * `proxy_routes` and the KV host blobs point at.
35
+ */
36
+ mismatched: number;
37
+ /**
38
+ * Hostnames that could not be reconciled — a throw, or a merge/writeback
39
+ * that failed without throwing. Each one is logged. A sweep reporting
40
+ * `updated: 0` is only healthy when this is 0 too.
41
+ */
42
+ errors: number;
43
+ }
44
+ /**
45
+ * Reconcile every custom hostname in the Cloudflare zone against the stored
46
+ * custom-domain rows.
47
+ *
48
+ * Without this, `status` and `verification` only ever refresh when someone
49
+ * reads a single domain by id (`get()`): `list()` and `getByDomain()` are
50
+ * deliberately DB-only so they stay fast and survive a Cloudflare outage. A
51
+ * hostname that finishes validation at the edge therefore stays `pending` in
52
+ * the database until a human happens to open its detail page. This closes that
53
+ * gap on a schedule.
54
+ *
55
+ * Enumerates Cloudflare-first — one paginated list call per 50 hostnames,
56
+ * rather than one request per stored domain — then resolves each hostname's
57
+ * tenant through `getByDomain`, which every adapter indexes because it is the
58
+ * request-routing path.
59
+ *
60
+ * One hostname's failure never aborts the sweep, and the result is returned
61
+ * rather than thrown: a cron that dies halfway leaves the rest of the zone
62
+ * stale until the next run.
63
+ *
64
+ * Deletion is deliberately out of scope. A hostname removed at the edge simply
65
+ * stops appearing in the listing, and "absent from a page I may have failed to
66
+ * fetch" is not evidence a domain is gone — removals go through `remove()`.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * export default {
71
+ * async scheduled(event, env) {
72
+ * const config = buildCloudflareConfig(env);
73
+ * console.log("custom domain sync", await syncCustomDomains(config));
74
+ * },
75
+ * };
76
+ * ```
77
+ */
78
+ export declare function syncCustomDomains(config: CloudflareConfig, options?: SyncCustomDomainsOptions): Promise<SyncCustomDomainsResult>;
@@ -10,6 +10,8 @@ export type { AnalyticsEngineActionExecutionsAdapterConfig };
10
10
  export type { CloudflareRateLimitBinding, CloudflareRateLimitBindings };
11
11
  export { createCloudflareRateLimitAdapter } from "./rate-limit";
12
12
  export type { CloudflareConfig };
13
+ export { syncCustomDomains } from "./customDomains/sync";
14
+ export type { SyncCustomDomainsOptions, SyncCustomDomainsResult, } from "./customDomains/sync";
13
15
  export { DispatchNamespaceCodeExecutor, type DispatchNamespaceCodeExecutorConfig, type DispatchNamespace, CloudflareCodeExecutor, type CloudflareCodeExecutorConfig, } from "./code-executor";
14
16
  export { generateWorkerScript } from "./code-executor/worker-template";
15
17
  export { WorkerLoaderCodeExecutor, type WorkerLoader, type WorkerCode, type WorkerStub, type WorkerLoaderCodeExecutorOptions, } from "./code-executor/worker-loader";
@@ -17,6 +19,8 @@ export { createAnalyticsEngineLogsAdapter } from "./analytics-engine-logs";
17
19
  export { createAnalyticsEngineStatsAdapter } from "./analytics-engine-logs";
18
20
  export { createAnalyticsEngineAnalyticsAdapter } from "./analytics-engine-logs";
19
21
  export { createAnalyticsEngineActionExecutionsAdapter } from "./analytics-engine-action-executions";
22
+ export { createAnalyticsEngineOutboxMetricsSink } from "./analytics-engine-outbox-metrics";
23
+ export type { AnalyticsEngineOutboxMetricsConfig, OutboxMetricRecord, } from "./analytics-engine-outbox-metrics";
20
24
  export { createR2SQLLogsAdapter } from "./r2-sql-logs";
21
25
  export { createR2SQLStatsAdapter } from "./r2-sql-logs";
22
26
  export { createCloudflareWfpD1Provisioner, createWfpProvisionerSteps, createWfpTenantProvisioningHook, createWfpForwardMiddleware, CloudflareApiClient, CloudflareApiError, } from "./wfp-provisioner";
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "type": "git",
12
12
  "url": "https://github.com/markusahlstrand/authhero"
13
13
  },
14
- "version": "3.0.12",
14
+ "version": "3.1.0",
15
15
  "files": [
16
16
  "dist"
17
17
  ],
@@ -47,8 +47,8 @@
47
47
  "typescript": "^5.9.3",
48
48
  "vite": "^8.0.14",
49
49
  "vitest": "^4.1.7",
50
- "@authhero/multi-tenancy": "15.0.0",
51
- "authhero": "9.9.1"
50
+ "@authhero/multi-tenancy": "15.0.1",
51
+ "authhero": "9.10.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@authhero/multi-tenancy": "^14.0.0",
@@ -67,8 +67,8 @@
67
67
  "dependencies": {
68
68
  "nanoid": "^5.1.11",
69
69
  "wretch": "^3.0.8",
70
- "@authhero/kysely-adapter": "12.6.1",
71
- "@authhero/adapter-interfaces": "4.11.0"
70
+ "@authhero/adapter-interfaces": "4.12.0",
71
+ "@authhero/kysely-adapter": "12.6.2"
72
72
  },
73
73
  "license": "AGPL-3.0-only",
74
74
  "scripts": {