repoaccess-core 0.2.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,14 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Gary Stupak
3
+
4
+ // Hand-written env-type augmentation - NOT generated, so `wrangler types` (which overwrites
5
+ // worker-configuration.d.ts) never clobbers it. Declaration-merges into the global CloudflareBindings.
6
+ //
7
+ // EVENT_WEBHOOK_SECRET is the OPTIONAL outbound-delivery signing secret (opt-in via EVENT_WEBHOOK_URL).
8
+ // It is intentionally NOT in wrangler.jsonc `secrets.required` - wrangler's `secrets` has no `optional`
9
+ // list, and listing it under `required` would force it set before every deploy. Defining `secrets`
10
+ // stops .dev.vars name-inference, so it would otherwise be untyped. Declare it OPTIONAL here so
11
+ // `env.EVENT_WEBHOOK_SECRET` is `string | undefined`; events.ts fail-closes when it is unset.
12
+ interface CloudflareBindings {
13
+ EVENT_WEBHOOK_SECRET?: string
14
+ }
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Gary Stupak
3
+
4
+ /**
5
+ * Cloudflare Workflows instance-id constraints. Verified against BOTH the public docs
6
+ * (workflows/build/workers-api: "up to 100 characters") and the bundled `workflows-shared`
7
+ * runtime that miniflare/production use:
8
+ *
9
+ * ALLOWED_STRING_ID_PATTERN = "^[a-zA-Z0-9_][a-zA-Z0-9-_]*$" (max 100 chars)
10
+ *
11
+ * Allowed: letters, digits, `_`, `-`. NOTABLY the colon `:` from the original id scheme's
12
+ * `{adapter}:{event_type}:{transaction_id}` is REJECTED ("Workflow instance has invalid id").
13
+ */
14
+ export const WORKFLOW_INSTANCE_ID_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_-]*$/
15
+ export const MAX_WORKFLOW_INSTANCE_ID_LENGTH = 100
16
+
17
+ export async function sha256Hex(input: string): Promise<string> {
18
+ const digest = await crypto.subtle.digest(
19
+ 'SHA-256',
20
+ new TextEncoder().encode(input),
21
+ )
22
+ return [...new Uint8Array(digest)]
23
+ .map((b) => b.toString(16).padStart(2, '0'))
24
+ .join('')
25
+ }
26
+
27
+ /**
28
+ * Build the deterministic Workflow instance id = the idempotency key. Components are
29
+ * joined with `-` (the `:` separator is rejected by the runtime). `adapter` and `eventType` come from a
30
+ * fixed, charset-safe vocabulary; `transactionId` is provider-defined and may (for some future
31
+ * provider) contain out-of-charset characters or be very long.
32
+ *
33
+ * Security-gate decision (availability): do NOT throw on such a transactionId - a
34
+ * thrown id denies a paying buyer access AND makes the webhook retry forever. Instead fall back to a
35
+ * SHA-256 hash of the transaction_id: always charset-valid, length-bounded, and collision-resistant
36
+ * (so dedupe stays correct - unlike lossy character-sanitization). Deterministic, so the same txn
37
+ * always yields the same id (idempotency holds). The readable form is kept for the common case
38
+ * (Stripe `payment_intent`, Paddle `txn_…` - both charset-safe).
39
+ */
40
+ export async function workflowInstanceId(
41
+ adapter: string,
42
+ eventType: string,
43
+ transactionId: string,
44
+ ): Promise<string> {
45
+ const readable = `${adapter}-${eventType}-${transactionId}`
46
+ if (
47
+ readable.length <= MAX_WORKFLOW_INSTANCE_ID_LENGTH &&
48
+ WORKFLOW_INSTANCE_ID_PATTERN.test(readable)
49
+ ) {
50
+ return readable
51
+ }
52
+ const digest = await sha256Hex(transactionId)
53
+ // adapter + eventType are controlled vocab (charset-safe); slice guards the pathological length.
54
+ return `${adapter}-${eventType}-${digest}`.slice(
55
+ 0,
56
+ MAX_WORKFLOW_INSTANCE_ID_LENGTH,
57
+ )
58
+ }
59
+
60
+ /**
61
+ * Deterministic Workflow id for an `api_callback` ping. The event_type and transaction_id are
62
+ * UNKNOWN before the entity is fetched (which happens in the Workflow, not on the ack path), so the
63
+ * id hashes the RAW ping body instead: identical retried pings collide → idempotent dedupe (the same
64
+ * `createBatch` guarantee as hmac); distinct events (sale vs refund vs dispute → different bodies)
65
+ * hash to distinct ids. `apicallback` is a fixed, charset-safe event-type slot. The SHA-256 hex is
66
+ * charset-safe and length-bounded; `adapter` is controlled vocab (slice guards a pathological name).
67
+ */
68
+ export async function apiCallbackInstanceId(
69
+ adapter: string,
70
+ rawBody: string,
71
+ ): Promise<string> {
72
+ const digest = await sha256Hex(rawBody)
73
+ return `${adapter}-apicallback-${digest}`.slice(
74
+ 0,
75
+ MAX_WORKFLOW_INSTANCE_ID_LENGTH,
76
+ )
77
+ }