@boopkit/cap-oauth-server 0.1.3 → 0.3.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.
Files changed (81) hide show
  1. package/capability.json +14 -2
  2. package/dist/authorization-code-service.d.ts +40 -11
  3. package/dist/authorization-code-service.d.ts.map +1 -1
  4. package/dist/authorization-code-service.js +93 -41
  5. package/dist/authorization-code-service.js.map +1 -1
  6. package/dist/bearer.d.ts +27 -4
  7. package/dist/bearer.d.ts.map +1 -1
  8. package/dist/bearer.js +4 -4
  9. package/dist/bearer.js.map +1 -1
  10. package/dist/client-prune.d.ts +102 -0
  11. package/dist/client-prune.d.ts.map +1 -0
  12. package/dist/client-prune.js +51 -0
  13. package/dist/client-prune.js.map +1 -0
  14. package/dist/client-registry-service.d.ts +78 -7
  15. package/dist/client-registry-service.d.ts.map +1 -1
  16. package/dist/client-registry-service.js +200 -52
  17. package/dist/client-registry-service.js.map +1 -1
  18. package/dist/config.d.ts +6 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +1 -0
  21. package/dist/config.js.map +1 -1
  22. package/dist/connections-controller.js +1 -1
  23. package/dist/connections-controller.js.map +1 -1
  24. package/dist/consent-controller.d.ts +80 -6
  25. package/dist/consent-controller.d.ts.map +1 -1
  26. package/dist/consent-controller.js +134 -18
  27. package/dist/consent-controller.js.map +1 -1
  28. package/dist/discovery-controller.d.ts +2 -1
  29. package/dist/discovery-controller.d.ts.map +1 -1
  30. package/dist/discovery-controller.js +14 -4
  31. package/dist/discovery-controller.js.map +1 -1
  32. package/dist/discovery.d.ts +26 -0
  33. package/dist/discovery.d.ts.map +1 -1
  34. package/dist/discovery.js +55 -1
  35. package/dist/discovery.js.map +1 -1
  36. package/dist/errors.d.ts +38 -0
  37. package/dist/errors.d.ts.map +1 -1
  38. package/dist/errors.js +59 -0
  39. package/dist/errors.js.map +1 -1
  40. package/dist/grant-service.d.ts +11 -4
  41. package/dist/grant-service.d.ts.map +1 -1
  42. package/dist/grant-service.js +6 -3
  43. package/dist/grant-service.js.map +1 -1
  44. package/dist/index.d.ts +10 -5
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +18 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/metadata-document.d.ts +22 -0
  49. package/dist/metadata-document.d.ts.map +1 -1
  50. package/dist/metadata-document.js +44 -2
  51. package/dist/metadata-document.js.map +1 -1
  52. package/dist/outbound-target.d.ts +48 -0
  53. package/dist/outbound-target.d.ts.map +1 -0
  54. package/dist/outbound-target.js +211 -0
  55. package/dist/outbound-target.js.map +1 -0
  56. package/dist/rate-limit.d.ts +53 -11
  57. package/dist/rate-limit.d.ts.map +1 -1
  58. package/dist/rate-limit.js +27 -4
  59. package/dist/rate-limit.js.map +1 -1
  60. package/dist/redirect-uri.d.ts +17 -9
  61. package/dist/redirect-uri.d.ts.map +1 -1
  62. package/dist/redirect-uri.js +26 -10
  63. package/dist/redirect-uri.js.map +1 -1
  64. package/dist/registration-controller.d.ts +10 -0
  65. package/dist/registration-controller.d.ts.map +1 -1
  66. package/dist/registration-controller.js +10 -0
  67. package/dist/registration-controller.js.map +1 -1
  68. package/dist/same-site.d.ts +54 -0
  69. package/dist/same-site.d.ts.map +1 -0
  70. package/dist/same-site.js +124 -0
  71. package/dist/same-site.js.map +1 -0
  72. package/dist/token-controller.d.ts +21 -0
  73. package/dist/token-controller.d.ts.map +1 -1
  74. package/dist/token-controller.js +42 -18
  75. package/dist/token-controller.js.map +1 -1
  76. package/package.json +2 -2
  77. package/scaffold/reference/oauth-server.ref.md +280 -13
  78. package/scaffold/src/middleware/oauth-server.ts +33 -17
  79. package/scaffold/src/scripts/prune-oauth-clients.ts +86 -0
  80. package/scaffold/src/services/oauth-server.service.ts +24 -0
  81. package/scaffold/views/oauth-consent-error.hbs +10 -3
package/capability.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "src/services/oauth-server.service.ts",
14
14
  "src/middleware/oauth-server.ts",
15
15
  "src/scripts/seed-oauth-clients.ts",
16
+ "src/scripts/prune-oauth-clients.ts",
16
17
  "views/oauth-consent.hbs",
17
18
  "views/oauth-consent-error.hbs",
18
19
  "views/oauth-connected-clients.hbs",
@@ -32,7 +33,7 @@
32
33
  "phase": "preRoutes",
33
34
  "order": 13,
34
35
  "code": "app.use(oauthDiscoveryRoutes);",
35
- "description": "serve /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource \u2014 mounted outside /api, and first among this app's capability mounts (the free slot between cap-web's requestLogger at 12 and cap-users' cookieParser at 14), because a client reads these before it holds any credential and must never meet a 401 here. The order sequences capability mounts against each other; an application whose own middleware sits above the @boopkit:preRoutes marker still runs that middleware first, so check that these two answer 200 to a caller holding nothing \u2014 which is what the verify step below does"
36
+ "description": "serve /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource (also at the RFC 9728 path-suffixed location, e.g. /.well-known/oauth-protected-resource/api/mcp, when OAUTH_RESOURCE_URL has a path) \u2014 mounted outside /api, and first among this app's capability mounts (the free slot between cap-web's requestLogger at 12 and cap-users' cookieParser at 14), because a client reads these before it holds any credential and must never meet a 401 here. The order sequences capability mounts against each other; an application whose own middleware sits above the @boopkit:preRoutes marker still runs that middleware first, so check that these two answer 200 to a caller holding nothing \u2014 which is what the verify step below does"
36
37
  },
37
38
  {
38
39
  "phase": "preRoutes",
@@ -45,6 +46,10 @@
45
46
  "oauth:seed-clients": {
46
47
  "cmd": "tsx src/scripts/seed-oauth-clients.ts",
47
48
  "description": "Register the static OAuth clients this deployment trusts. Idempotent."
49
+ },
50
+ "oauth:prune-clients": {
51
+ "cmd": "tsx src/scripts/prune-oauth-clients.ts",
52
+ "description": "Delete abandoned client registrations \u2014 unauthenticated rows over 30 days old that nobody has connected. Schedule it daily."
48
53
  }
49
54
  },
50
55
  "env": [
@@ -78,7 +83,7 @@
78
83
  },
79
84
  {
80
85
  "name": "OAUTH_MAX_REGISTERED_CLIENTS",
81
- "description": "Ceiling on rows created by open dynamic registration (default: 1000). /oauth/register is unauthenticated by design, so this is the hard bound on what an abusive caller can write; the per-IP rate limit only makes reaching it slow.",
86
+ "description": "Ceiling on rows created by open dynamic registration (default: 1000). /oauth/register is unauthenticated by design, so this is the hard bound on what an abusive caller can write; the per-IP rate limit only makes reaching it slow. It is a bound, not a collector \u2014 schedule npm run oauth:prune-clients, or rows nobody ever connected hold their slots forever and the ceiling eventually refuses legitimate registrations.",
82
87
  "default": "1000",
83
88
  "required": "runtime"
84
89
  },
@@ -88,6 +93,12 @@
88
93
  "default": "10",
89
94
  "required": "runtime"
90
95
  },
96
+ {
97
+ "name": "OAUTH_METADATA_RESOLUTION_RATE_LIMIT",
98
+ "description": "First-sight Client ID Metadata Document resolutions allowed per hour, per source address (default: 10). GET /oauth/authorize registers a CIMD client the first time it sees one, which is an unauthenticated write and an outbound fetch, so it carries the same per-IP limit /oauth/register does. Only a client_id with no row yet is charged, so an ordinary visitor to a connector this server already knows never spends any of it.",
99
+ "default": "10",
100
+ "required": "runtime"
101
+ },
91
102
  {
92
103
  "name": "OAUTH_DEVICE_CODE_ENABLED",
93
104
  "description": "Set to the string \"true\" to enable the RFC 8628 device-code grant. Off by default. When off the endpoints are not mounted at all and the grant type is absent from the discovery metadata, rather than advertised against an endpoint that answers 404 \u2014 a client that read the metadata would treat that 404 as a server fault rather than as an unsupported feature. Not yet implemented; declared here so the flag has one name.",
@@ -112,6 +123,7 @@
112
123
  "Add styles/oauth-server.css to your stylesheet entry point and rebuild Tailwind, or the consent screen and the connected-clients screen render unstyled.",
113
124
  "Protect the surface you are exposing to connectors: app.use('/api/mcp', resolveOAuthBearer, requireOAuth('profile'), yourRoutes). Both are exported from src/middleware/oauth-server.ts, which also mounts GET /api/oauth/whoami as a worked example you can curl with a fresh access token. Do not mount resolveOAuthBearer on a router that already carries cap-users' resolveBearer \u2014 pick the door that router's callers use.",
114
125
  "Edit src/scripts/seed-oauth-clients.ts with the clients you want to trust by name, then run npm run oauth:seed-clients. Clients that register themselves \u2014 anything supporting RFC 7591 or publishing a Client ID Metadata Document \u2014 need no entry there.",
126
+ "Schedule npm run oauth:prune-clients to run daily \u2014 Heroku Scheduler, cron, or a cap-background-jobs consumer. It deletes unauthenticated registrations over 30 days old that nobody has connected. Unscheduled, OAUTH_MAX_REGISTERED_CLIENTS is the only bound on registry growth, and a deployment that reaches the ceiling starts refusing legitimate registrations.",
115
127
  "Decide whether open registration suits you. POST /oauth/register is unauthenticated by design, which is what the Claude ecosystem expects; if you would rather only vouch for clients yourself, remove the preRoutes hook that mounts oauthServerRoutes and keep the seed script.",
116
128
  "Link to /connections from wherever your account settings live. The screen lists what a user has connected, states that a connected application reaches every account they belong to, and sends a disconnect through its own confirmation page. It is mounted already and gated by requireAuth; what it needs from you is a way in."
117
129
  ],
@@ -1,5 +1,5 @@
1
1
  import { type TokenService } from './token-service.js';
2
- import type { PropertyStore } from './user-store.js';
2
+ import { type PropertyStore } from './user-store.js';
3
3
  /**
4
4
  * The authorization code: one identity row, one property entry.
5
5
  *
@@ -14,14 +14,34 @@ import type { PropertyStore } from './user-store.js';
14
14
  * is a SHA-256 digest, which is what the identity row already stores; the raw
15
15
  * code exists only in the redirect the browser followed, and nowhere at rest.
16
16
  *
17
- * ## The entry survives redemption, and that is the point
17
+ * ## The entry survives redemption, and the identity row stays live
18
18
  *
19
- * Redeeming stamps `redeemed_at` on the entry and end-dates the identity row —
20
- * it does not delete the entry. A deleted entry leaves nothing behind, so a
21
- * replay is an unexplained failed request; a retained one records that the code
22
- * was redeemed and when, which is what makes the replay an observable event
23
- * rather than merely an unsuccessful one. The entry is dropped only once it has
24
- * expired, on the next write that passes over it.
19
+ * Redeeming stamps `redeemed_at` on the entry. It does not delete the entry,
20
+ * and — since boop-hnbv — it does not end-date the identity row either. Both
21
+ * halves of that are one decision, and the second half is the surprising one.
22
+ *
23
+ * `redeemed_at` is the single-use gate. The row is what carries the **user id**,
24
+ * and cap-users' `(domain, key)` lookup is active-only, so end-dating the row at
25
+ * redemption threw the owner away: a replay arriving a second later resolved to
26
+ * nothing and could only be answered `unknown_code`. The retained entry was
27
+ * never read, because there was no user to read it under — which made the
28
+ * replay-detection apparatus this data model was shaped around dead code.
29
+ *
30
+ * Leaving the row live until its own 120-second expiry keeps the owner
31
+ * reachable, so a replay resolves, finds the stamped entry, and is answered
32
+ * `already_redeemed` **with the user id and the client it was issued to** — which
33
+ * is what the token endpoint needs to do what RFC 6749 §4.1.2 and §10.5 require:
34
+ * revoke the tokens the first redemption minted. Single-use is unchanged; it is
35
+ * enforced by `redeemed_at` now rather than by the tombstone.
36
+ *
37
+ * The live row grants nothing on its own. Every path to it goes through this
38
+ * service, and this service refuses a stamped entry. It is the same shape the
39
+ * refresh-token rotation uses one domain over, and for the same reason: a
40
+ * credential whose row is gone is indistinguishable from one that never existed,
41
+ * and telling those apart is the entire point.
42
+ *
43
+ * The entry is dropped once it has expired, on the next write that passes over
44
+ * it, and `resolve` end-dates the row the next time it reads it past expiry.
25
45
  *
26
46
  * ## Expiry here is `expires_at`, not the identity-row rule
27
47
  *
@@ -45,7 +65,7 @@ export interface AuthorizationCodeRecord {
45
65
  resource: string | null;
46
66
  issued_at: string;
47
67
  expires_at: string;
48
- /** Set on redemption; the entry stays so a replay is visible. */
68
+ /** Set on redemption. The single-use gate, and the record of the replay. */
49
69
  redeemed_at: string | null;
50
70
  }
51
71
  /** What redemption needs to know, beyond the code itself. */
@@ -64,7 +84,15 @@ export type RedeemResult = {
64
84
  } | {
65
85
  ok: false;
66
86
  reason: RedeemFailure;
87
+ /** Present whenever the code resolved to somebody — see `redeem`. */
67
88
  userId?: number;
89
+ /**
90
+ * The client the code was ISSUED to, which is not always the one that
91
+ * presented it. A replay answered on a `client_id` of the attacker's
92
+ * choosing must still revoke the grant the code actually belongs to,
93
+ * and never a bystander's.
94
+ */
95
+ clientPublicId?: string;
68
96
  };
69
97
  export interface IssueCodeInput {
70
98
  userId: number;
@@ -83,8 +111,9 @@ export interface AuthorizationCodeService {
83
111
  record: AuthorizationCodeRecord;
84
112
  }>;
85
113
  /**
86
- * Redeem once. End-dates the identity row and stamps `redeemed_at`; a second
87
- * call answers `already_redeemed` rather than `unknown_code`.
114
+ * Redeem once. Stamps `redeemed_at` and leaves the identity row live, so a
115
+ * second call answers `already_redeemed` — carrying the owner and the client,
116
+ * so the caller can revoke what the first redemption issued.
88
117
  */
89
118
  redeem(input: RedeemCodeInput): Promise<RedeemResult>;
90
119
  /** The entries a user currently holds. Used by the credentials-at-rest test. */
@@ -1 +1 @@
1
- {"version":3,"file":"authorization-code-service.d.ts","sourceRoot":"","sources":["../src/authorization-code-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAClD,eAAO,MAAM,uBAAuB,+BAA+B,CAAC;AAEpE,MAAM,WAAW,uBAAuB;IACtC,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4EAA4E;AAC5E,MAAM,MAAM,aAAa,GACrB,cAAc,GACd,kBAAkB,GAClB,SAAS,GACT,iBAAiB,GACjB,uBAAuB,CAAC;AAE5B,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,GAC7D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,uBAAuB,CAAA;KAAE,CAAC,CAAC;IACzF;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACtD,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,+BAA+B;IAC9C,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACjE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CA2I1B"}
1
+ {"version":3,"file":"authorization-code-service.d.ts","sourceRoot":"","sources":["../src/authorization-code-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,eAAO,MAAM,oBAAoB,gBAAgB,CAAC;AAClD,eAAO,MAAM,uBAAuB,+BAA+B,CAAC;AAEpE,MAAM,WAAW,uBAAuB;IACtC,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4EAA4E;AAC5E,MAAM,MAAM,aAAa,GACrB,cAAc,GACd,kBAAkB,GAClB,SAAS,GACT,iBAAiB,GACjB,uBAAuB,CAAC;AAE5B,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,GAC7D;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,aAAa,CAAC;IACtB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEN,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,uBAAuB,CAAA;KAAE,CAAC,CAAC;IACzF;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACtD,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,+BAA+B;IAC9C,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACjE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,+BAA+B,GACvC,wBAAwB,CAuK1B"}
@@ -4,6 +4,7 @@ exports.OAUTH_CODES_INFO_SCHEMA = exports.OAUTH_CODES_PROPERTY = void 0;
4
4
  exports.createAuthorizationCodeService = createAuthorizationCodeService;
5
5
  const property_array_js_1 = require("./property-array.js");
6
6
  const token_service_js_1 = require("./token-service.js");
7
+ const user_store_js_1 = require("./user-store.js");
7
8
  /**
8
9
  * The authorization code: one identity row, one property entry.
9
10
  *
@@ -18,14 +19,34 @@ const token_service_js_1 = require("./token-service.js");
18
19
  * is a SHA-256 digest, which is what the identity row already stores; the raw
19
20
  * code exists only in the redirect the browser followed, and nowhere at rest.
20
21
  *
21
- * ## The entry survives redemption, and that is the point
22
+ * ## The entry survives redemption, and the identity row stays live
22
23
  *
23
- * Redeeming stamps `redeemed_at` on the entry and end-dates the identity row —
24
- * it does not delete the entry. A deleted entry leaves nothing behind, so a
25
- * replay is an unexplained failed request; a retained one records that the code
26
- * was redeemed and when, which is what makes the replay an observable event
27
- * rather than merely an unsuccessful one. The entry is dropped only once it has
28
- * expired, on the next write that passes over it.
24
+ * Redeeming stamps `redeemed_at` on the entry. It does not delete the entry,
25
+ * and — since boop-hnbv — it does not end-date the identity row either. Both
26
+ * halves of that are one decision, and the second half is the surprising one.
27
+ *
28
+ * `redeemed_at` is the single-use gate. The row is what carries the **user id**,
29
+ * and cap-users' `(domain, key)` lookup is active-only, so end-dating the row at
30
+ * redemption threw the owner away: a replay arriving a second later resolved to
31
+ * nothing and could only be answered `unknown_code`. The retained entry was
32
+ * never read, because there was no user to read it under — which made the
33
+ * replay-detection apparatus this data model was shaped around dead code.
34
+ *
35
+ * Leaving the row live until its own 120-second expiry keeps the owner
36
+ * reachable, so a replay resolves, finds the stamped entry, and is answered
37
+ * `already_redeemed` **with the user id and the client it was issued to** — which
38
+ * is what the token endpoint needs to do what RFC 6749 §4.1.2 and §10.5 require:
39
+ * revoke the tokens the first redemption minted. Single-use is unchanged; it is
40
+ * enforced by `redeemed_at` now rather than by the tombstone.
41
+ *
42
+ * The live row grants nothing on its own. Every path to it goes through this
43
+ * service, and this service refuses a stamped entry. It is the same shape the
44
+ * refresh-token rotation uses one domain over, and for the same reason: a
45
+ * credential whose row is gone is indistinguishable from one that never existed,
46
+ * and telling those apart is the entire point.
47
+ *
48
+ * The entry is dropped once it has expired, on the next write that passes over
49
+ * it, and `resolve` end-dates the row the next time it reads it past expiry.
29
50
  *
30
51
  * ## Expiry here is `expires_at`, not the identity-row rule
31
52
  *
@@ -71,26 +92,23 @@ function createAuthorizationCodeService(options) {
71
92
  if (!identityKey)
72
93
  return { ok: false, reason: 'unknown_code' };
73
94
  // Resolving through the token service applies the identity row's
74
- // active-only lookup, so a code redeemed a moment ago (row end-dated)
75
- // misses here — and the property entry below is what turns that miss into
76
- // `already_redeemed` rather than `unknown_code`.
95
+ // active-only lookup. Redemption no longer end-dates the row, so a code
96
+ // redeemed a moment ago still resolves and reaches the `redeemed_at`
97
+ // check below — which is the whole point of the change (see the header).
77
98
  const resolved = await tokenService.resolve(token_service_js_1.OAUTH_CODE_DOMAIN, input.code);
78
99
  const at = now();
79
100
  if (!resolved) {
80
- // The row is gone: never issued, already redeemed and end-dated, or
81
- // expired and swept. All three are `unknown_code` here, and that is the
82
- // honest answer rather than a shortcut. The row is what carries the
83
- // user id, so once it is end-dated there is nothing left to look the
84
- // owner up by — cap-users' lookup is active-only and the identity
85
- // boundary forbids reaching past it. RFC 6749 gives all three the same
86
- // wire answer (`invalid_grant`) for the same reason: telling a caller
87
- // which one it hit tells it whether the code was ever real.
101
+ // The row is gone: never issued, malformed, or past its 120 seconds and
102
+ // swept. All of them are `unknown_code`, and RFC 6749 gives all of them
103
+ // the same wire answer (`invalid_grant`) for the same reason — telling a
104
+ // caller which one it hit tells it whether the code was ever real.
88
105
  //
89
- // What the retained entry buys is the record. A deleted entry leaves
90
- // nothing behind; the entry kept with `redeemed_at` set means an
91
- // operator reading `oauth-codes` can see that this code WAS redeemed,
92
- // and when — which is what turns a replay from an unexplained failed
93
- // request into an observable event.
106
+ // A replay is NOT in that list any more. Once the code's own window has
107
+ // closed there is no user id left to look the owner up by, so there is
108
+ // nothing this branch could revoke even if it wanted to; but by then the
109
+ // tokens the first redemption minted are the attacker's problem, not an
110
+ // undetected replay — a code two minutes stale is an expiry, and every
111
+ // replay worth catching arrives inside the window.
94
112
  return { ok: false, reason: 'unknown_code' };
95
113
  }
96
114
  const snapshot = await (0, property_array_js_1.readArray)(propertyStore, resolved.userId, exports.OAUTH_CODES_PROPERTY);
@@ -100,12 +118,22 @@ function createAuthorizationCodeService(options) {
100
118
  // scope set, so there is nothing to check the request against.
101
119
  if (!record)
102
120
  return { ok: false, reason: 'unknown_code', userId: resolved.userId };
103
- // Reachable while the row is still live: a second redemption that arrives
104
- // between the property write and the end-date below, which is exactly the
105
- // concurrent-replay case. Across time the row is already ended and the
106
- // branch above answers instead.
121
+ // The replay branch, and the reason the row is left live. It answers every
122
+ // second presentation inside the code's window, whether it arrives a
123
+ // millisecond or a minute after the first.
124
+ //
125
+ // It is checked BEFORE the client and redirect-URI comparisons on purpose:
126
+ // a code that has already been spent is spent whatever the replay claims
127
+ // about itself, and an attacker must not be able to dodge the revocation
128
+ // by presenting a stolen code under some other client's name. The client
129
+ // reported back is the one the code was ISSUED to, for the same reason.
107
130
  if (record.redeemed_at !== null) {
108
- return { ok: false, reason: 'already_redeemed', userId: resolved.userId };
131
+ return {
132
+ ok: false,
133
+ reason: 'already_redeemed',
134
+ userId: resolved.userId,
135
+ clientPublicId: record.client_public_id,
136
+ };
109
137
  }
110
138
  if (Date.parse(record.expires_at) <= at.getTime()) {
111
139
  return { ok: false, reason: 'expired', userId: resolved.userId };
@@ -116,21 +144,45 @@ function createAuthorizationCodeService(options) {
116
144
  if (!matchRedirectUri(record.redirect_uri, input.redirectUri)) {
117
145
  return { ok: false, reason: 'redirect_uri_mismatch', userId: resolved.userId };
118
146
  }
119
- // Order matters, and this is the order.
147
+ // The single-use claim, and the only one there is: `redeemed_at` on the
148
+ // entry, written under `expectedRevision` so a second redemption that read
149
+ // the same snapshot is refused rather than silently applied.
120
150
  //
121
- // The property write goes first because it is the ATOMIC claim: it quotes
122
- // `expectedRevision`, so two simultaneous redemptions of one code cannot
123
- // both succeed — the loser is refused by the revision check rather than
124
- // silently applied. End-dating the row first would leave that race open,
125
- // because `endDate` has no compare-and-set of its own.
151
+ // With one honest caveat, since a comment here used to call this atomic
152
+ // without qualification. cap-services' `put` compares the revision and
153
+ // then updates in a separate statement, so two writers that interleave
154
+ // between the two can both land (boop-8fcd). That is a defect one layer
155
+ // down and it is not specific to codes — every property write in every
156
+ // capability shares it. What this code can promise is the sequential case,
157
+ // which is the replay an attacker can actually aim: present the code
158
+ // again, any time inside its window, and it is refused and the grant is
159
+ // revoked.
126
160
  const redeemed = { ...record, redeemed_at: at.toISOString() };
127
- await (0, property_array_js_1.writeArray)(propertyStore, resolved.userId, exports.OAUTH_CODES_PROPERTY,
128
- // The redeemed entry is kept until its own window closes; `live` drops
129
- // only entries that have actually expired.
130
- [...live(snapshot.entries, at).filter((e) => e.code_key !== identityKey), redeemed], snapshot, { infoSchema: exports.OAUTH_CODES_INFO_SCHEMA });
131
- // And then the tombstone, so the code is spent across time as well as
132
- // within the request.
133
- await tokenService.revokeByKey(token_service_js_1.OAUTH_CODE_DOMAIN, identityKey);
161
+ try {
162
+ await (0, property_array_js_1.writeArray)(propertyStore, resolved.userId, exports.OAUTH_CODES_PROPERTY,
163
+ // The redeemed entry is kept until its own window closes; `live` drops
164
+ // only entries that have actually expired.
165
+ [...live(snapshot.entries, at).filter((e) => e.code_key !== identityKey), redeemed], snapshot, { infoSchema: exports.OAUTH_CODES_INFO_SCHEMA });
166
+ }
167
+ catch (err) {
168
+ if (!(0, user_store_js_1.isStaleWriteError)(err))
169
+ throw err;
170
+ // The dead heat: two redemptions of one code both read the entry before
171
+ // either wrote it, and this one lost the revision check. That is a
172
+ // replay like any other — the only difference is that it was detected by
173
+ // the write rather than by the read — so it is answered the same way,
174
+ // with the owner and the client, rather than thrown for the caller to
175
+ // translate into a bare `invalid_grant` it can do nothing about.
176
+ return {
177
+ ok: false,
178
+ reason: 'already_redeemed',
179
+ userId: resolved.userId,
180
+ clientPublicId: record.client_public_id,
181
+ };
182
+ }
183
+ // No tombstone on the row. `redeemed_at` above is the single-use gate now,
184
+ // and the row stays live so the owner is still reachable when the replay
185
+ // comes — see the header.
134
186
  return { ok: true, userId: resolved.userId, record: redeemed };
135
187
  },
136
188
  async list(userId) {
@@ -1 +1 @@
1
- {"version":3,"file":"authorization-code-service.js","sourceRoot":"","sources":["../src/authorization-code-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authorization-code-service.js","sourceRoot":"","sources":["../src/authorization-code-service.ts"],"names":[],"mappings":"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"}
package/dist/bearer.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RequestHandler } from 'express';
2
- import type { AuthMiddlewareDeps, BaseUserInfo, User } from '@boopkit/cap-users';
2
+ import type { BaseUserInfo, User } from '@boopkit/cap-users';
3
3
  import './express.js';
4
4
  import type { GrantService, OAuthGrantRecord } from './grant-service.js';
5
5
  import { type TokenService } from './token-service.js';
@@ -49,10 +49,33 @@ export interface ResolvedOAuthPrincipal {
49
49
  grant: OAuthGrantRecord;
50
50
  clientPublicId: string;
51
51
  }
52
+ /**
53
+ * Everything the resource server needs, and nothing else.
54
+ *
55
+ * The user read is a single injected function. It used to be that **and** a
56
+ * separate `AuthMiddlewareDeps` argument — cap-users' `{ authService,
57
+ * identityService, userService }` trio — which was handed to
58
+ * `createBearerMiddleware` and never read by it (boop-9ral). An application
59
+ * therefore supplied its user layer twice: once as `findUserById`, which the
60
+ * lookup below actually calls, and once as a `userService` inside a parameter
61
+ * nobody touched.
62
+ *
63
+ * Dropping it is the same narrowing `user-store.ts` applies to the identity and
64
+ * property services: what this capability can ask for stays smaller than what
65
+ * cap-users offers, so an application resolving users through a cache, a test
66
+ * harness or a service of its own owes this package one function rather than a
67
+ * fully constructed installation.
68
+ */
52
69
  export interface OAuthBearerOptions {
53
70
  tokenService: TokenService;
54
71
  grantService: GrantService;
55
- /** cap-users' `userService.findById`, injected rather than reached for. */
72
+ /**
73
+ * How this application turns a user id into the principal `req.user` carries.
74
+ *
75
+ * cap-users' `userService.findById` is the usual answer and the one the
76
+ * scaffold passes — but it is injected rather than reached for, so it need not
77
+ * be cap-users' at all.
78
+ */
56
79
  findUserById(userId: number): Promise<User<BaseUserInfo> | null>;
57
80
  /**
58
81
  * The `WWW-Authenticate` value on a 401.
@@ -83,7 +106,7 @@ export declare function createOAuthTokenResolver(options: OAuthBearerOptions): (
83
106
  * the binding is made per request. That is a closure allocation, not work; the
84
107
  * three reads inside it are the cost of the request either way.
85
108
  */
86
- export declare function createOAuthBearerMiddleware(deps: AuthMiddlewareDeps, options: OAuthBearerOptions): RequestHandler;
109
+ export declare function createOAuthBearerMiddleware(options: OAuthBearerOptions): RequestHandler;
87
110
  /** The challenge sent when the request carried no usable OAuth token at all. */
88
111
  export declare const DEFAULT_BEARER_CHALLENGE = "Bearer";
89
112
  /**
@@ -142,5 +165,5 @@ export interface OAuthResourceServer {
142
165
  /** Then gate them: `requireOAuth('profile')`. */
143
166
  requireOAuth(...scopes: string[]): RequestHandler;
144
167
  }
145
- export declare function createOAuthResourceServer(deps: AuthMiddlewareDeps, options: OAuthBearerOptions): OAuthResourceServer;
168
+ export declare function createOAuthResourceServer(options: OAuthBearerOptions): OAuthResourceServer;
146
169
  //# sourceMappingURL=bearer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAE/E,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAEjF,OAAO,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,EAAE,gBAAgB,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;IACjE;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAe3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,kBAAkB,GAC1B,cAAc,CAwBhB;AAED,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAG/F;AAcD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,GAAG,cAAc,EAAE,MAAM,EAAE,GAAG,cAAc,CAgCxE;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,aAAa,EAAE,cAAc,CAAC;IAC9B,iDAAiD;IACjD,YAAY,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC;CACnD;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,kBAAkB,GAC1B,mBAAmB,CAMrB"}
1
+ {"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,EAAE,gBAAgB,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;IACjE;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAe3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAwBvF;AAED,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAG/F;AAcD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,GAAG,cAAc,EAAE,MAAM,EAAE,GAAG,cAAc,CAgCxE;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,aAAa,EAAE,cAAc,CAAC;IAC9B,iDAAiD;IACjD,YAAY,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC;CACnD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CAM1F"}
package/dist/bearer.js CHANGED
@@ -42,7 +42,7 @@ function createOAuthTokenResolver(options) {
42
42
  * the binding is made per request. That is a closure allocation, not work; the
43
43
  * three reads inside it are the cost of the request either way.
44
44
  */
45
- function createOAuthBearerMiddleware(deps, options) {
45
+ function createOAuthBearerMiddleware(options) {
46
46
  const resolveOAuthToken = createOAuthTokenResolver(options);
47
47
  const challenge = options.challenge ?? 'Bearer';
48
48
  return function oauthBearer(req, res, next) {
@@ -51,7 +51,7 @@ function createOAuthBearerMiddleware(deps, options) {
51
51
  // caller who presented nothing at all — the path on which the resolver
52
52
  // itself never refuses anybody.
53
53
  req.oauthChallenge = challenge;
54
- const middleware = (0, cap_users_1.createBearerMiddleware)(deps, {
54
+ const middleware = (0, cap_users_1.createBearerMiddleware)({
55
55
  strategy: 'oauth',
56
56
  challenge,
57
57
  resolve: async (token) => {
@@ -140,10 +140,10 @@ function requireOAuth(...requiredScopes) {
140
140
  next();
141
141
  };
142
142
  }
143
- function createOAuthResourceServer(deps, options) {
143
+ function createOAuthResourceServer(options) {
144
144
  const challenge = options.challenge ?? exports.DEFAULT_BEARER_CHALLENGE;
145
145
  return {
146
- resolveBearer: createOAuthBearerMiddleware(deps, { ...options, challenge }),
146
+ resolveBearer: createOAuthBearerMiddleware({ ...options, challenge }),
147
147
  requireOAuth,
148
148
  };
149
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ import type { ClientRegistryService } from './client-registry-service.js';
2
+ import { type GrantService } from './grant-service.js';
3
+ import type { TokenService } from './token-service.js';
4
+ import type { OauthClient } from './types.js';
5
+ /**
6
+ * Garbage collection for the client registry — the caller `pruneAbandonedClients`
7
+ * was always waiting for.
8
+ *
9
+ * ## The two halves the registry deliberately does not own
10
+ *
11
+ * `ClientRegistryService.pruneAbandonedClients({ before, isInUse })` knows how to
12
+ * pick candidates: stranger-created rows (`dynamic` and `metadata_document`)
13
+ * older than a cutoff. It knows nothing about grants, and it must not — the
14
+ * registry's whole discipline is that a client is software and a grant is a
15
+ * relationship between a person and that software, and the registry never learns
16
+ * who consented. So it takes the "is this in use?" question as a parameter and
17
+ * answers "no" by default, which is safe only in a deployment that has no grants
18
+ * at all.
19
+ *
20
+ * This file supplies the missing half, and a call site for it. Both live here
21
+ * rather than in the registry for the same reason the registry asked: answering
22
+ * the question means reading `oauth-grants`, which is a user-shaped read.
23
+ *
24
+ * ## What "in use" means, and why it is the grant rather than the token
25
+ *
26
+ * A client is in use when **somebody still has it connected** — that is, when
27
+ * some user's `oauth-grants` holds a live entry naming its `publicId`.
28
+ *
29
+ * The identity rows cannot carry that answer, in either direction, and
30
+ * `revocation-service.ts` makes the whole argument: an ended row does not mean
31
+ * disconnected, because ordinary expiry end-dates rows too, and a live row does
32
+ * not mean connected, because rotation leaves the superseded refresh row
33
+ * deliberately active. Reading tokens would delete a client half the deployment
34
+ * uses, or keep one nobody has used in a year. The grant record is the signal.
35
+ *
36
+ * Token rows are still how the *users* are found — `findUserIdsForClient` scans
37
+ * the `"<publicId>|"` key prefix the wire format was shaped to make cheap — and
38
+ * that is sound because a grant is only ever written at redemption, in the same
39
+ * request that mints the first token. A grant with no token row behind it is not
40
+ * a state this server can reach.
41
+ *
42
+ * ## What it deliberately does not protect
43
+ *
44
+ * An authorization code issued against a client whose row is about to be swept.
45
+ * Codes live for a minute and the cutoff is thirty days, so the window is a
46
+ * client that registered a month ago, never connected, and is mid-flow at the
47
+ * exact moment the sweep runs; it loses that one redemption and can register
48
+ * again. Closing it properly would mean scanning `oauth-codes`, which is a
49
+ * per-user property with no cross-user read — the cost is a full user scan to buy
50
+ * a minute of protection for a client that has never completed a grant.
51
+ *
52
+ * ## Deleting a row a grant still points at is fine, and intended
53
+ *
54
+ * An *ended* grant is not in use, so its client may be swept. The
55
+ * connected-clients screen already renders a grant whose registry row has gone —
56
+ * it falls back to the `client_id` the client presented and says the application
57
+ * is no longer registered here — because a connection the screen does not list is
58
+ * a connection nobody can disconnect.
59
+ */
60
+ export interface ClientInUseOptions {
61
+ grantService: GrantService;
62
+ tokenService: TokenService;
63
+ }
64
+ /**
65
+ * The real `isInUse` predicate: is anybody still connected to this client?
66
+ *
67
+ * Suitable for passing straight to `pruneAbandonedClients`. It is `false` for a
68
+ * client nobody ever connected and for one every user has disconnected, and
69
+ * `true` the moment one live grant names the client's `publicId`.
70
+ */
71
+ export declare function createClientInUsePredicate(options: ClientInUseOptions): (client: OauthClient) => Promise<boolean>;
72
+ /** What one sweep did, in the shape a log line or a job result wants. */
73
+ export interface ClientPruneResult {
74
+ /** The cutoff actually used. Rows created before this were candidates. */
75
+ before: Date;
76
+ /** The `client_id` of every row deleted, in the order they were deleted. */
77
+ removed: string[];
78
+ }
79
+ export interface ClientPruneOptions extends ClientInUseOptions {
80
+ clientRegistry: ClientRegistryService;
81
+ /**
82
+ * How old an unconnected row has to be before it is garbage. Defaults to the
83
+ * plan's thirty days (`DEFAULT_ABANDONED_CLIENT_TTL_MS`).
84
+ */
85
+ ttlMs?: number;
86
+ /** Injectable clock, so a test does not have to wait a month. */
87
+ now?: () => Date;
88
+ }
89
+ export interface ClientPruneJob {
90
+ /** Sweep once. Safe to run concurrently with the rest of the server. */
91
+ run(): Promise<ClientPruneResult>;
92
+ }
93
+ /**
94
+ * The scheduled sweep, as one callable object.
95
+ *
96
+ * A job rather than a loose function because the thing that was missing was a
97
+ * *call site*: a scaffolded application schedules `run()` — from its own cron,
98
+ * from Heroku Scheduler, or from a cap-background-jobs consumer — and the
99
+ * capability owns which rows go and which stay.
100
+ */
101
+ export declare function createClientPruneJob(options: ClientPruneOptions): ClientPruneJob;
102
+ //# sourceMappingURL=client-prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-prune.d.ts","sourceRoot":"","sources":["../src/client-prune.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,kBAAkB,GAC1B,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAmB3C;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,MAAM,EAAE,IAAI,CAAC;IACb,4EAA4E;IAC5E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,cAAc,EAAE,qBAAqB,CAAC;IACtC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,GAAG,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAahF"}