@_mustachio/openauth 0.11.0 → 0.12.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/dist/esm/adapters/d1/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +23 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +27 -27
- package/dist/esm/domain/authorize.js +2 -2
- package/dist/esm/domain/callback.js +121 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +2 -2
- package/dist/esm/domain/dpop.js +4 -4
- package/dist/esm/domain/id-token.js +5 -5
- package/dist/esm/domain/jwt.js +4 -4
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +80 -1
- package/dist/esm/domain/par.js +2 -2
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token.js +5 -5
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +3 -19
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/metadata.js +2 -2
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/middleware/tenant.js +6 -6
- package/dist/esm/http/schemas/authorize.js +2 -2
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +33 -32
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +208 -0
- package/dist/esm/methods/saml-sp/attributes.js +75 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +70 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +69 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +3 -0
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +3 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +704 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +204 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +18 -5
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +23 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +71 -0
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +39 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/domain/callback.ts +215 -7
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +190 -1
- package/src/http/context.ts +6 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +2 -21
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/method-route.ts +75 -2
- package/src/index.ts +3 -0
- package/src/methods/saml-sp/acs.ts +399 -0
- package/src/methods/saml-sp/attributes.ts +137 -0
- package/src/methods/saml-sp/authnrequest.ts +128 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +118 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +204 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
- package/src/methods/saml-sp/saml-instance.ts +173 -0
- package/src/methods/saml-sp/slo-initiate.ts +177 -0
- package/src/methods/saml-sp/sls.ts +306 -0
- package/src/methods/saml-sp/types.ts +203 -0
- package/src/ports/CONSISTENCY.md +25 -0
- package/src/ports/audit-log.ts +18 -5
- package/src/ports/session-store.ts +28 -0
- package/src/types/idp.ts +74 -0
- package/src/types/method.ts +110 -0
|
@@ -54,6 +54,37 @@ export type AuthMethod<P = unknown, S = unknown> = {
|
|
|
54
54
|
* same factory get distinct URL spaces).
|
|
55
55
|
*/
|
|
56
56
|
routes: Record<string, MethodHandler<P, S>>;
|
|
57
|
+
/**
|
|
58
|
+
* Opt-in allowlist of route keys (same `"GET /metadata"` string form
|
|
59
|
+
* as `routes` keys) that the framework dispatches **without** a flow
|
|
60
|
+
* cookie or flow record — anonymous, unauthenticated GETs. The sole
|
|
61
|
+
* intended use is publishing static, per-instance descriptive
|
|
62
|
+
* documents (SAML SP metadata XML). The handler receives
|
|
63
|
+
* `ctx.flow === null` and `ctx.methodState === null` and MUST be a
|
|
64
|
+
* pure function of `ctx.tenant` + `ctx.dispatch` + its captured
|
|
65
|
+
* config — it must not assume an authenticated principal.
|
|
66
|
+
*
|
|
67
|
+
* Absent (the default for every method) ⇒ behaviour is unchanged:
|
|
68
|
+
* every `/m/<id>/*` request requires the `idp.flow` cookie. The
|
|
69
|
+
* cookie gate is skipped *only* for a route key a method explicitly
|
|
70
|
+
* lists here — fail-closed by construction.
|
|
71
|
+
*/
|
|
72
|
+
publicRoutes?: ReadonlyArray<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Opt-in: this method instance handles **unsolicited** upstream
|
|
75
|
+
* callbacks — a `POST /cb/<methodId>` carrying a provider assertion
|
|
76
|
+
* with no framework state envelope and no prior flow (SAML
|
|
77
|
+
* IdP-initiated SSO). When `true`, the framework, instead of
|
|
78
|
+
* rejecting a stateless callback, dispatches `GET /callback` with
|
|
79
|
+
* `flow === null` and a derived `dispatch` (issuer/ACS); the handler
|
|
80
|
+
* verifies the assertion and returns `success` **with
|
|
81
|
+
* `unsolicitedBinding`**. Absent (every method's default) ⇒ a
|
|
82
|
+
* stateless callback stays an `invalid_request` (the conservative
|
|
83
|
+
* default — many deployments do not want IdP-initiated). General
|
|
84
|
+
* capability, not SAML-specific; set per instance (a SAML instance
|
|
85
|
+
* sets it only when its config enables IdP-initiated).
|
|
86
|
+
*/
|
|
87
|
+
unsolicitedCallback?: boolean;
|
|
57
88
|
/**
|
|
58
89
|
* Token-exchange function for the `/token` endpoint when the method
|
|
59
90
|
* participates in client-credentials-style flows (e.g. `m2m`). Most
|
|
@@ -96,6 +127,46 @@ export type MethodContext<S = unknown> = {
|
|
|
96
127
|
* and the relevant data is on `flow`).
|
|
97
128
|
*/
|
|
98
129
|
dispatch: MethodDispatchData | null;
|
|
130
|
+
/**
|
|
131
|
+
* Per-method-instance scratch storage scoped to
|
|
132
|
+
* `(tenant.id, method.id)`. Survives across flows — distinct from
|
|
133
|
+
* `methodState`, which is per-flow.
|
|
134
|
+
*
|
|
135
|
+
* Most methods do NOT need this. It exists for cross-flow
|
|
136
|
+
* deduplication patterns such as SAML SP assertion-ID replay
|
|
137
|
+
* protection.
|
|
138
|
+
*
|
|
139
|
+
* Backed by `SessionStore.{saveScratch,readScratch,deleteScratch}`
|
|
140
|
+
* when those optional methods are implemented. Against adapters that
|
|
141
|
+
* don't implement them, every call returns
|
|
142
|
+
* `{ ok: false, error: unsupported }` — the method should surface a
|
|
143
|
+
* `MethodResult.error` with a clear message, not silently degrade.
|
|
144
|
+
*/
|
|
145
|
+
methodScratch: MethodScratch;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Caller-facing API for per-method-instance scratch. The framework
|
|
149
|
+
* scopes user-supplied keys with a `(tenantId, methodId)` prefix before
|
|
150
|
+
* delegating to `SessionStore` — adapters never see raw method keys.
|
|
151
|
+
*
|
|
152
|
+
* Values are UTF-8 strings; methods JSON-encode if they want to stash
|
|
153
|
+
* objects. Keeping the port-level type narrow simplifies adapter
|
|
154
|
+
* implementations (one TEXT column, one Dynamo `S` attribute, etc.).
|
|
155
|
+
*/
|
|
156
|
+
export type MethodScratch = {
|
|
157
|
+
/**
|
|
158
|
+
* Persist `value` under `key` with the given TTL. Overwrites prior
|
|
159
|
+
* value for the same key. `ttlMs` must be positive.
|
|
160
|
+
*/
|
|
161
|
+
put(key: string, value: string, ttlMs: number): Promise<Result<void>>;
|
|
162
|
+
/**
|
|
163
|
+
* Read the value previously stored at `key`. Returns `unknown_state`
|
|
164
|
+
* if the key is missing or expired (the underlying adapter MAY
|
|
165
|
+
* lazily evict expired entries on read).
|
|
166
|
+
*/
|
|
167
|
+
get(key: string): Promise<Result<string>>;
|
|
168
|
+
/** Idempotent. Resolves `ok` whether the key existed or not. */
|
|
169
|
+
delete(key: string): Promise<Result<void>>;
|
|
99
170
|
};
|
|
100
171
|
/** Framework-supplied data available to the method at `/authorize` time. */
|
|
101
172
|
export type MethodDispatchData = {
|
|
@@ -135,6 +206,27 @@ export type MethodResult<P = unknown, S = unknown> =
|
|
|
135
206
|
saveMethodState?: S;
|
|
136
207
|
/** Serialized into a `Cache-Control` header by the framework. */
|
|
137
208
|
cache?: CachePolicy;
|
|
209
|
+
/**
|
|
210
|
+
* Verified upstream-logout notification. Set **only** by a
|
|
211
|
+
* flowless **public** logout route (e.g. SAML front-channel SLS)
|
|
212
|
+
* once it has cryptographically verified the inbound logout
|
|
213
|
+
* message. The method stays port-free — it proves authenticity
|
|
214
|
+
* and builds the protocol response (`response` = the signed
|
|
215
|
+
* `LogoutResponse` redirect / ack); the privileged side effect
|
|
216
|
+
* runs in the framework.
|
|
217
|
+
*
|
|
218
|
+
* When present on a public route the framework fires
|
|
219
|
+
* `IdPOptions.onLogout` and, if that returns `{ revokeSubject }`,
|
|
220
|
+
* runs `revokeAllForSubject` — then returns `response`. Ignored
|
|
221
|
+
* on non-public (flow-bearing) routes: an authenticated method
|
|
222
|
+
* route never logs anyone out. Mirrors the Phase 2 V′ pattern
|
|
223
|
+
* (`success.unsolicitedBinding`): an optional field on an
|
|
224
|
+
* existing variant, not a new `MethodResult` kind.
|
|
225
|
+
*/
|
|
226
|
+
logout?: {
|
|
227
|
+
nameId?: string;
|
|
228
|
+
sessionIndex?: string;
|
|
229
|
+
};
|
|
138
230
|
}
|
|
139
231
|
/**
|
|
140
232
|
* Authentication succeeded. The HTTP layer hands `providerSubject` +
|
|
@@ -147,6 +239,23 @@ export type MethodResult<P = unknown, S = unknown> =
|
|
|
147
239
|
providerSubject: string;
|
|
148
240
|
properties: P;
|
|
149
241
|
setCookies?: SetCookie[];
|
|
242
|
+
/**
|
|
243
|
+
* RP binding for a **flowless** (unsolicited / IdP-initiated)
|
|
244
|
+
* authentication. Consulted **only** when the method ran with no
|
|
245
|
+
* `flow` (e.g. an unsolicited SAML Response — see
|
|
246
|
+
* `AuthMethod.unsolicitedCallback`). On the normal SP-initiated
|
|
247
|
+
* path a `flow` exists and this is ignored; on the flowless path
|
|
248
|
+
* it is **required** (the framework has no pending RP request to
|
|
249
|
+
* read `client_id` / `redirect_uri` / `scope` from, so the method
|
|
250
|
+
* supplies the operator-configured defaults). The framework still
|
|
251
|
+
* validates `clientId` / `redirectUri` against the tenant's
|
|
252
|
+
* registered client before issuing a code.
|
|
253
|
+
*/
|
|
254
|
+
unsolicitedBinding?: {
|
|
255
|
+
clientId: string;
|
|
256
|
+
redirectUri: string;
|
|
257
|
+
scopes: string[];
|
|
258
|
+
};
|
|
150
259
|
}
|
|
151
260
|
/** User refused / failed auth in a non-error way (e.g. consent declined). */
|
|
152
261
|
| {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../../../src/types/method.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,uBAAuB,CAAA;AAE/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEnE;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,IAAI;IACjD;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAA;IACV;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,UAAU,CAAA;IAChB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3C;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,EAAE,CAAC,IAAI,CAChC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,KAClB,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAEhC;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,OAAO,IAAI;IACvC,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAA;IAChB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,aAAa,CAAA;IACrB,2EAA2E;IAC3E,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,WAAW,EAAE,CAAC,GAAG,IAAI,CAAA;IACrB,6DAA6D;IAC7D,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC;;;;;;OAMG;IACH,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../../../src/types/method.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_mustachio/openauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
"import": "./dist/esm/adapters/*/index.js",
|
|
38
38
|
"types": "./dist/types/adapters/*/index.d.ts"
|
|
39
39
|
},
|
|
40
|
+
"./methods/saml-sp": {
|
|
41
|
+
"node": {
|
|
42
|
+
"import": "./dist/esm/methods/saml-sp/index.js",
|
|
43
|
+
"types": "./dist/types/methods/saml-sp/index.d.ts"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
40
46
|
"./*": {
|
|
41
47
|
"import": "./dist/esm/*.js",
|
|
42
48
|
"types": "./dist/types/*.d.ts"
|
|
@@ -73,8 +79,10 @@
|
|
|
73
79
|
},
|
|
74
80
|
"dependencies": {
|
|
75
81
|
"@noble/hashes": "2.2.0",
|
|
82
|
+
"@node-saml/node-saml": "5.1.0",
|
|
76
83
|
"@simplewebauthn/server": "13.1.1",
|
|
77
84
|
"@standard-schema/spec": "1.0.0-beta.3",
|
|
85
|
+
"@xmldom/xmldom": "0.8.13",
|
|
78
86
|
"aws4fetch": "1.0.20",
|
|
79
87
|
"jose": "5.9.6",
|
|
80
88
|
"oauth4webapi": "3.8.6",
|
|
@@ -67,6 +67,11 @@ export const INITIAL_SCHEMA_SQL = [
|
|
|
67
67
|
payload TEXT NOT NULL,
|
|
68
68
|
expires_at INTEGER NOT NULL
|
|
69
69
|
)`,
|
|
70
|
+
`CREATE TABLE IF NOT EXISTS openauth_scratch (
|
|
71
|
+
scratch_key TEXT PRIMARY KEY,
|
|
72
|
+
value TEXT NOT NULL,
|
|
73
|
+
expires_at INTEGER NOT NULL
|
|
74
|
+
)`,
|
|
70
75
|
`CREATE TABLE IF NOT EXISTS openauth_audit_events (
|
|
71
76
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
77
|
ts INTEGER NOT NULL,
|
|
@@ -227,4 +227,72 @@ export class D1SessionStore implements SessionStore {
|
|
|
227
227
|
}
|
|
228
228
|
return ok(undefined)
|
|
229
229
|
}
|
|
230
|
+
|
|
231
|
+
async saveScratch(
|
|
232
|
+
key: string,
|
|
233
|
+
value: string,
|
|
234
|
+
ttlMs: number,
|
|
235
|
+
): Promise<Result<void>> {
|
|
236
|
+
if (ttlMs <= 0) {
|
|
237
|
+
return err(
|
|
238
|
+
authError.internalError(
|
|
239
|
+
`saveScratch: ttlMs must be positive, got ${ttlMs}`,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
const expiresAt = this.#clock() + ttlMs
|
|
244
|
+
try {
|
|
245
|
+
// Pinned to the primary — scratch backs InResponseTo replay
|
|
246
|
+
// protection, which is read-after-write sensitive at the ACS.
|
|
247
|
+
await primarySession(this.#db)
|
|
248
|
+
.prepare(
|
|
249
|
+
`INSERT INTO openauth_scratch (scratch_key, value, expires_at) VALUES (?1, ?2, ?3)
|
|
250
|
+
ON CONFLICT(scratch_key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`,
|
|
251
|
+
)
|
|
252
|
+
.bind(key, value, expiresAt)
|
|
253
|
+
.run()
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return err(authError.internalError("saveScratch: upsert failed", e))
|
|
256
|
+
}
|
|
257
|
+
return ok(undefined)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async readScratch(key: string): Promise<Result<string>> {
|
|
261
|
+
let row: { value: string; expires_at: number } | null
|
|
262
|
+
try {
|
|
263
|
+
row = await primarySession(this.#db)
|
|
264
|
+
.prepare(
|
|
265
|
+
`SELECT value, expires_at FROM openauth_scratch WHERE scratch_key = ?1`,
|
|
266
|
+
)
|
|
267
|
+
.bind(key)
|
|
268
|
+
.first<{ value: string; expires_at: number }>()
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return err(authError.internalError("readScratch: query failed", e))
|
|
271
|
+
}
|
|
272
|
+
if (!row) {
|
|
273
|
+
return err(authError.unknownState(`scratch "${key}" unknown`))
|
|
274
|
+
}
|
|
275
|
+
if (this.#clock() >= Number(row.expires_at)) {
|
|
276
|
+
try {
|
|
277
|
+
await primarySession(this.#db)
|
|
278
|
+
.prepare(`DELETE FROM openauth_scratch WHERE scratch_key = ?1`)
|
|
279
|
+
.bind(key)
|
|
280
|
+
.run()
|
|
281
|
+
} catch {}
|
|
282
|
+
return err(authError.unknownState(`scratch "${key}" expired`))
|
|
283
|
+
}
|
|
284
|
+
return ok(row.value)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async deleteScratch(key: string): Promise<Result<void>> {
|
|
288
|
+
try {
|
|
289
|
+
await primarySession(this.#db)
|
|
290
|
+
.prepare(`DELETE FROM openauth_scratch WHERE scratch_key = ?1`)
|
|
291
|
+
.bind(key)
|
|
292
|
+
.run()
|
|
293
|
+
} catch (e) {
|
|
294
|
+
return err(authError.internalError("deleteScratch: delete failed", e))
|
|
295
|
+
}
|
|
296
|
+
return ok(undefined)
|
|
297
|
+
}
|
|
230
298
|
}
|
|
@@ -51,6 +51,11 @@ type StoredSession = {
|
|
|
51
51
|
expiresAt: number
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
type StoredScratch = {
|
|
55
|
+
value: string
|
|
56
|
+
expiresAt: number
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
export type DurableObjectSessionStoreOptions = {
|
|
55
60
|
storage: DurableObjectStorageLike
|
|
56
61
|
clock?: () => number
|
|
@@ -218,10 +223,66 @@ export class DurableObjectSessionStore implements SessionStore {
|
|
|
218
223
|
}
|
|
219
224
|
return ok(undefined)
|
|
220
225
|
}
|
|
226
|
+
|
|
227
|
+
async saveScratch(
|
|
228
|
+
key: string,
|
|
229
|
+
value: string,
|
|
230
|
+
ttlMs: number,
|
|
231
|
+
): Promise<Result<void>> {
|
|
232
|
+
if (ttlMs <= 0) {
|
|
233
|
+
return err(
|
|
234
|
+
authError.internalError(
|
|
235
|
+
`saveScratch: ttlMs must be positive, got ${ttlMs}`,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
const stored: StoredScratch = {
|
|
240
|
+
value,
|
|
241
|
+
expiresAt: this.#clock() + ttlMs,
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
// Plain put — scratch overwrites any prior value for the key. No
|
|
245
|
+
// transaction needed: it is a single write, and the DO serializes
|
|
246
|
+
// all operations on this instance.
|
|
247
|
+
await this.#storage.put(scratchKey(key), stored)
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return err(authError.internalError("saveScratch: put failed", e))
|
|
250
|
+
}
|
|
251
|
+
return ok(undefined)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async readScratch(key: string): Promise<Result<string>> {
|
|
255
|
+
let stored: StoredScratch | undefined
|
|
256
|
+
try {
|
|
257
|
+
stored = await this.#storage.get<StoredScratch>(scratchKey(key))
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return err(authError.internalError("readScratch: get failed", e))
|
|
260
|
+
}
|
|
261
|
+
if (!stored) {
|
|
262
|
+
return err(authError.unknownState(`scratch "${key}" unknown`))
|
|
263
|
+
}
|
|
264
|
+
if (this.#clock() >= stored.expiresAt) {
|
|
265
|
+
try {
|
|
266
|
+
await this.#storage.delete(scratchKey(key))
|
|
267
|
+
} catch {}
|
|
268
|
+
return err(authError.unknownState(`scratch "${key}" expired`))
|
|
269
|
+
}
|
|
270
|
+
return ok(stored.value)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async deleteScratch(key: string): Promise<Result<void>> {
|
|
274
|
+
try {
|
|
275
|
+
await this.#storage.delete(scratchKey(key))
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return err(authError.internalError("deleteScratch: delete failed", e))
|
|
278
|
+
}
|
|
279
|
+
return ok(undefined)
|
|
280
|
+
}
|
|
221
281
|
}
|
|
222
282
|
|
|
223
283
|
const flowKey = (flowId: string) => `flow:${flowId}`
|
|
224
284
|
const sessionKey = (sessionId: string) => `session:${sessionId}`
|
|
285
|
+
const scratchKey = (key: string) => `scratch:${key}`
|
|
225
286
|
|
|
226
287
|
// Re-exported for adapter users wiring `storage.transaction` callbacks.
|
|
227
288
|
export type { DurableObjectStorageLike, DurableObjectTransactionLike }
|
|
@@ -211,6 +211,69 @@ export class DynamoSessionStore implements SessionStore {
|
|
|
211
211
|
}
|
|
212
212
|
return ok(undefined)
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
async saveScratch(
|
|
216
|
+
key: string,
|
|
217
|
+
value: string,
|
|
218
|
+
ttlMs: number,
|
|
219
|
+
): Promise<Result<void>> {
|
|
220
|
+
if (ttlMs <= 0) {
|
|
221
|
+
return err(
|
|
222
|
+
authError.internalError(
|
|
223
|
+
`saveScratch: ttlMs must be positive, got ${ttlMs}`,
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
const expiresAtMs = this.#clock() + ttlMs
|
|
228
|
+
try {
|
|
229
|
+
// Unconditional put — scratch overwrites any prior value for the key.
|
|
230
|
+
await this.#exec.put({
|
|
231
|
+
item: {
|
|
232
|
+
pk: "scratch",
|
|
233
|
+
sk: key,
|
|
234
|
+
value,
|
|
235
|
+
expires_at: expiresAtMs,
|
|
236
|
+
ttl: Math.floor(expiresAtMs / 1000),
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return err(authError.internalError("saveScratch: put failed", e))
|
|
241
|
+
}
|
|
242
|
+
return ok(undefined)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async readScratch(key: string): Promise<Result<string>> {
|
|
246
|
+
let row: Record<string, unknown> | undefined
|
|
247
|
+
try {
|
|
248
|
+
row = await this.#exec.get({
|
|
249
|
+
key: { pk: "scratch", sk: key },
|
|
250
|
+
consistentRead: true,
|
|
251
|
+
})
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return err(authError.internalError("readScratch: get failed", e))
|
|
254
|
+
}
|
|
255
|
+
if (!row) {
|
|
256
|
+
return err(authError.unknownState(`scratch "${key}" unknown`))
|
|
257
|
+
}
|
|
258
|
+
// Native DynamoDB TTL eviction is best-effort (can lag up to ~48h), so
|
|
259
|
+
// filter on the adapter clock for correctness.
|
|
260
|
+
if (this.#clock() >= Number(row.expires_at)) {
|
|
261
|
+
try {
|
|
262
|
+
await this.#exec.delete({ key: { pk: "scratch", sk: key } })
|
|
263
|
+
} catch {}
|
|
264
|
+
return err(authError.unknownState(`scratch "${key}" expired`))
|
|
265
|
+
}
|
|
266
|
+
return ok(String(row.value))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async deleteScratch(key: string): Promise<Result<void>> {
|
|
270
|
+
try {
|
|
271
|
+
await this.#exec.delete({ key: { pk: "scratch", sk: key } })
|
|
272
|
+
} catch (e) {
|
|
273
|
+
return err(authError.internalError("deleteScratch: delete failed", e))
|
|
274
|
+
}
|
|
275
|
+
return ok(undefined)
|
|
276
|
+
}
|
|
214
277
|
}
|
|
215
278
|
|
|
216
279
|
function parseFlowPayload(raw: unknown): FlowRecord {
|
|
@@ -28,6 +28,11 @@ type StoredPar = {
|
|
|
28
28
|
expiresAt: number
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
type StoredScratch = {
|
|
32
|
+
value: string
|
|
33
|
+
expiresAt: number
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
export type MemorySessionStoreOptions = {
|
|
32
37
|
clock?: Clock
|
|
33
38
|
}
|
|
@@ -37,6 +42,7 @@ export class MemorySessionStore implements SessionStore {
|
|
|
37
42
|
#flows = new Map<string, StoredFlow>()
|
|
38
43
|
#sessions = new Map<string, SessionRecord>()
|
|
39
44
|
#par = new Map<string, StoredPar>()
|
|
45
|
+
#scratch = new Map<string, StoredScratch>()
|
|
40
46
|
|
|
41
47
|
constructor(opts: MemorySessionStoreOptions = {}) {
|
|
42
48
|
this.#clock = opts.clock ?? realClock
|
|
@@ -168,4 +174,37 @@ export class MemorySessionStore implements SessionStore {
|
|
|
168
174
|
}
|
|
169
175
|
return ok(stored.record)
|
|
170
176
|
}
|
|
177
|
+
|
|
178
|
+
async saveScratch(
|
|
179
|
+
key: string,
|
|
180
|
+
value: string,
|
|
181
|
+
ttlMs: number,
|
|
182
|
+
): Promise<Result<void>> {
|
|
183
|
+
if (ttlMs <= 0) {
|
|
184
|
+
return err(
|
|
185
|
+
authError.internalError(
|
|
186
|
+
`saveScratch: ttlMs must be positive, got ${ttlMs}`,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
this.#scratch.set(key, { value, expiresAt: this.#clock() + ttlMs })
|
|
191
|
+
return ok(undefined)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async readScratch(key: string): Promise<Result<string>> {
|
|
195
|
+
const stored = this.#scratch.get(key)
|
|
196
|
+
if (!stored) {
|
|
197
|
+
return err(authError.unknownState(`scratch "${key}" unknown`))
|
|
198
|
+
}
|
|
199
|
+
if (this.#clock() >= stored.expiresAt) {
|
|
200
|
+
this.#scratch.delete(key)
|
|
201
|
+
return err(authError.unknownState(`scratch "${key}" expired`))
|
|
202
|
+
}
|
|
203
|
+
return ok(stored.value)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async deleteScratch(key: string): Promise<Result<void>> {
|
|
207
|
+
this.#scratch.delete(key)
|
|
208
|
+
return ok(undefined)
|
|
209
|
+
}
|
|
171
210
|
}
|
|
@@ -89,6 +89,12 @@ CREATE TABLE IF NOT EXISTS openauth_sessions (
|
|
|
89
89
|
expires_at bigint NOT NULL
|
|
90
90
|
);
|
|
91
91
|
|
|
92
|
+
CREATE TABLE IF NOT EXISTS openauth_scratch (
|
|
93
|
+
scratch_key text PRIMARY KEY,
|
|
94
|
+
value text NOT NULL,
|
|
95
|
+
expires_at bigint NOT NULL
|
|
96
|
+
);
|
|
97
|
+
|
|
92
98
|
CREATE TABLE IF NOT EXISTS openauth_audit_events (
|
|
93
99
|
id bigserial PRIMARY KEY,
|
|
94
100
|
ts bigint NOT NULL,
|
|
@@ -203,6 +203,75 @@ export class PostgresSessionStore implements SessionStore {
|
|
|
203
203
|
}
|
|
204
204
|
return ok(undefined)
|
|
205
205
|
}
|
|
206
|
+
|
|
207
|
+
async saveScratch(
|
|
208
|
+
key: string,
|
|
209
|
+
value: string,
|
|
210
|
+
ttlMs: number,
|
|
211
|
+
): Promise<Result<void>> {
|
|
212
|
+
if (ttlMs <= 0) {
|
|
213
|
+
return err(
|
|
214
|
+
authError.internalError(
|
|
215
|
+
`saveScratch: ttlMs must be positive, got ${ttlMs}`,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
const expiresAt = this.#clock() + ttlMs
|
|
220
|
+
try {
|
|
221
|
+
await this.#exec.query(
|
|
222
|
+
`INSERT INTO openauth_scratch (scratch_key, value, expires_at)
|
|
223
|
+
VALUES ($1, $2, $3)
|
|
224
|
+
ON CONFLICT (scratch_key)
|
|
225
|
+
DO UPDATE SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,
|
|
226
|
+
[key, value, expiresAt],
|
|
227
|
+
)
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return err(authError.internalError("saveScratch: upsert failed", e))
|
|
230
|
+
}
|
|
231
|
+
return ok(undefined)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async readScratch(key: string): Promise<Result<string>> {
|
|
235
|
+
let row: { value: string; expires_at: string | number } | undefined
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.#exec.query<{
|
|
238
|
+
value: string
|
|
239
|
+
expires_at: string | number
|
|
240
|
+
}>(
|
|
241
|
+
`SELECT value, expires_at FROM openauth_scratch WHERE scratch_key = $1`,
|
|
242
|
+
[key],
|
|
243
|
+
)
|
|
244
|
+
row = result.rows[0]
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return err(authError.internalError("readScratch: query failed", e))
|
|
247
|
+
}
|
|
248
|
+
if (!row) {
|
|
249
|
+
return err(authError.unknownState(`scratch "${key}" unknown`))
|
|
250
|
+
}
|
|
251
|
+
if (this.#clock() >= Number(row.expires_at)) {
|
|
252
|
+
// Lazy GC — best-effort, don't fail the read if the delete races.
|
|
253
|
+
try {
|
|
254
|
+
await this.#exec.query(
|
|
255
|
+
`DELETE FROM openauth_scratch WHERE scratch_key = $1`,
|
|
256
|
+
[key],
|
|
257
|
+
)
|
|
258
|
+
} catch {}
|
|
259
|
+
return err(authError.unknownState(`scratch "${key}" expired`))
|
|
260
|
+
}
|
|
261
|
+
return ok(row.value)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async deleteScratch(key: string): Promise<Result<void>> {
|
|
265
|
+
try {
|
|
266
|
+
await this.#exec.query(
|
|
267
|
+
`DELETE FROM openauth_scratch WHERE scratch_key = $1`,
|
|
268
|
+
[key],
|
|
269
|
+
)
|
|
270
|
+
} catch (e) {
|
|
271
|
+
return err(authError.internalError("deleteScratch: delete failed", e))
|
|
272
|
+
}
|
|
273
|
+
return ok(undefined)
|
|
274
|
+
}
|
|
206
275
|
}
|
|
207
276
|
|
|
208
277
|
function parseFlowRecord(raw: unknown): FlowRecord {
|