@boopkit/cap-oauth-server 0.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.
Files changed (124) hide show
  1. package/capability.json +120 -0
  2. package/dist/authorization-code-service.d.ts +105 -0
  3. package/dist/authorization-code-service.d.ts.map +1 -0
  4. package/dist/authorization-code-service.js +142 -0
  5. package/dist/authorization-code-service.js.map +1 -0
  6. package/dist/bearer.d.ts +146 -0
  7. package/dist/bearer.d.ts.map +1 -0
  8. package/dist/bearer.js +150 -0
  9. package/dist/bearer.js.map +1 -0
  10. package/dist/body-field.d.ts +24 -0
  11. package/dist/body-field.d.ts.map +1 -0
  12. package/dist/body-field.js +43 -0
  13. package/dist/body-field.js.map +1 -0
  14. package/dist/client-registry-service.d.ts +120 -0
  15. package/dist/client-registry-service.d.ts.map +1 -0
  16. package/dist/client-registry-service.js +450 -0
  17. package/dist/client-registry-service.js.map +1 -0
  18. package/dist/config.d.ts +82 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +157 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/connections-controller.d.ts +106 -0
  23. package/dist/connections-controller.d.ts.map +1 -0
  24. package/dist/connections-controller.js +161 -0
  25. package/dist/connections-controller.js.map +1 -0
  26. package/dist/consent-controller.d.ts +197 -0
  27. package/dist/consent-controller.d.ts.map +1 -0
  28. package/dist/consent-controller.js +532 -0
  29. package/dist/consent-controller.js.map +1 -0
  30. package/dist/discovery-controller.d.ts +16 -0
  31. package/dist/discovery-controller.d.ts.map +1 -0
  32. package/dist/discovery-controller.js +52 -0
  33. package/dist/discovery-controller.js.map +1 -0
  34. package/dist/discovery.d.ts +95 -0
  35. package/dist/discovery.d.ts.map +1 -0
  36. package/dist/discovery.js +101 -0
  37. package/dist/discovery.js.map +1 -0
  38. package/dist/errors.d.ts +26 -0
  39. package/dist/errors.d.ts.map +1 -0
  40. package/dist/errors.js +41 -0
  41. package/dist/errors.js.map +1 -0
  42. package/dist/express.d.ts +66 -0
  43. package/dist/express.d.ts.map +1 -0
  44. package/dist/express.js +3 -0
  45. package/dist/express.js.map +1 -0
  46. package/dist/grant-service.d.ts +102 -0
  47. package/dist/grant-service.d.ts.map +1 -0
  48. package/dist/grant-service.js +137 -0
  49. package/dist/grant-service.js.map +1 -0
  50. package/dist/index.d.ts +67 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +148 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/metadata-document.d.ts +60 -0
  55. package/dist/metadata-document.d.ts.map +1 -0
  56. package/dist/metadata-document.js +148 -0
  57. package/dist/metadata-document.js.map +1 -0
  58. package/dist/oauth-error.d.ts +45 -0
  59. package/dist/oauth-error.d.ts.map +1 -0
  60. package/dist/oauth-error.js +51 -0
  61. package/dist/oauth-error.js.map +1 -0
  62. package/dist/pkce.d.ts +29 -0
  63. package/dist/pkce.d.ts.map +1 -0
  64. package/dist/pkce.js +65 -0
  65. package/dist/pkce.js.map +1 -0
  66. package/dist/property-array.d.ts +60 -0
  67. package/dist/property-array.d.ts.map +1 -0
  68. package/dist/property-array.js +31 -0
  69. package/dist/property-array.js.map +1 -0
  70. package/dist/public-id.d.ts +35 -0
  71. package/dist/public-id.d.ts.map +1 -0
  72. package/dist/public-id.js +76 -0
  73. package/dist/public-id.js.map +1 -0
  74. package/dist/rate-limit.d.ts +56 -0
  75. package/dist/rate-limit.d.ts.map +1 -0
  76. package/dist/rate-limit.js +55 -0
  77. package/dist/rate-limit.js.map +1 -0
  78. package/dist/redirect-uri.d.ts +89 -0
  79. package/dist/redirect-uri.d.ts.map +1 -0
  80. package/dist/redirect-uri.js +249 -0
  81. package/dist/redirect-uri.js.map +1 -0
  82. package/dist/registration-controller.d.ts +62 -0
  83. package/dist/registration-controller.d.ts.map +1 -0
  84. package/dist/registration-controller.js +79 -0
  85. package/dist/registration-controller.js.map +1 -0
  86. package/dist/request-client.d.ts +19 -0
  87. package/dist/request-client.d.ts.map +1 -0
  88. package/dist/request-client.js +32 -0
  89. package/dist/request-client.js.map +1 -0
  90. package/dist/revocation-controller.d.ts +28 -0
  91. package/dist/revocation-controller.d.ts.map +1 -0
  92. package/dist/revocation-controller.js +176 -0
  93. package/dist/revocation-controller.js.map +1 -0
  94. package/dist/revocation-service.d.ts +113 -0
  95. package/dist/revocation-service.d.ts.map +1 -0
  96. package/dist/revocation-service.js +121 -0
  97. package/dist/revocation-service.js.map +1 -0
  98. package/dist/token-controller.d.ts +79 -0
  99. package/dist/token-controller.d.ts.map +1 -0
  100. package/dist/token-controller.js +207 -0
  101. package/dist/token-controller.js.map +1 -0
  102. package/dist/token-service.d.ts +175 -0
  103. package/dist/token-service.d.ts.map +1 -0
  104. package/dist/token-service.js +251 -0
  105. package/dist/token-service.js.map +1 -0
  106. package/dist/types.d.ts +130 -0
  107. package/dist/types.d.ts.map +1 -0
  108. package/dist/types.js +13 -0
  109. package/dist/types.js.map +1 -0
  110. package/dist/user-store.d.ts +110 -0
  111. package/dist/user-store.d.ts.map +1 -0
  112. package/dist/user-store.js +40 -0
  113. package/dist/user-store.js.map +1 -0
  114. package/package.json +37 -0
  115. package/scaffold/prisma/schema/oauth-clients.prisma +80 -0
  116. package/scaffold/reference/oauth-server.ref.md +478 -0
  117. package/scaffold/src/middleware/oauth-server.ts +227 -0
  118. package/scaffold/src/scripts/seed-oauth-clients.ts +72 -0
  119. package/scaffold/src/services/oauth-server.service.ts +101 -0
  120. package/scaffold/styles/oauth-server.css +345 -0
  121. package/scaffold/views/oauth-connected-clients.hbs +98 -0
  122. package/scaffold/views/oauth-consent-error.hbs +22 -0
  123. package/scaffold/views/oauth-consent.hbs +138 -0
  124. package/scaffold/views/oauth-disconnect-confirm.hbs +57 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The two `.well-known` discovery documents.
3
+ *
4
+ * ## Computed, never read off disk
5
+ *
6
+ * The donor controller (`paywalls-site/src/controllers/wellknownController.ts`)
7
+ * serves these by joining `req.params.resource` onto a fixed prefix and
8
+ * compiling whatever file it lands on. Two properties of that are wrong for a
9
+ * shipped package rather than for paywalls, and the findings name both:
10
+ *
11
+ * 1. `req.params.resource` reaches `path.join` without `..` being stripped.
12
+ * Paywalls is saved by there being nothing outside the prefix it would
13
+ * serve, which is a fact about its filesystem, not a property of its code.
14
+ * 2. The prefix is relative to the process working directory, which is fine
15
+ * for an application and fragile for a capability whose files may be
16
+ * scaffolded or may sit in `node_modules`.
17
+ *
18
+ * Neither applies here, because these documents are **computed from
19
+ * configuration** by the functions below. There is no path, so there is nothing
20
+ * to traverse, and there is no working directory to be wrong about.
21
+ *
22
+ * ## And why the values are all fresh
23
+ *
24
+ * The donor's documents do not survive at all. Its authorization-server
25
+ * document advertises `grant_types_supported: ["refresh_token"]` and nothing
26
+ * else, has no `code_challenge_methods_supported`, no
27
+ * `client_id_metadata_document_supported`, and points `authorization_endpoint`
28
+ * at a POST endpoint authenticated by an API key. Its protected-resource
29
+ * document sets `resource` to the bare host rather than to the protected
30
+ * endpoint URL, which is the one thing the Claude ecosystem checklist says must
31
+ * match exactly as clients enter it.
32
+ */
33
+ export interface DiscoveryConfig {
34
+ /** The authorization server's issuer identifier — an https origin, no path. */
35
+ issuer: string;
36
+ /**
37
+ * The protected resource's URL, as clients enter it.
38
+ *
39
+ * This is *not* the issuer. A client that connects to `https://app/api/mcp`
40
+ * needs to read that exact string back, so this is configured rather than
41
+ * derived, and the derivation the scaffold offers is a default rather than a
42
+ * rule.
43
+ */
44
+ resource: string;
45
+ /** Scopes this server will issue. `offline_access` earns the refresh token. */
46
+ scopesSupported: string[];
47
+ /**
48
+ * Whether the device-code grant is mounted.
49
+ *
50
+ * When it is off the grant type is **absent** from `grant_types_supported`,
51
+ * rather than advertised against an endpoint that answers 404. Discovery
52
+ * metadata is a contract: a client that read the metadata treats a 404 as a
53
+ * server fault rather than as an unsupported feature. The flag gates the
54
+ * mount and this field from one value, so the two cannot drift.
55
+ *
56
+ * Slice 1 has no device endpoints, so the scaffold passes `false`.
57
+ */
58
+ deviceCodeEnabled?: boolean;
59
+ /** Optional human-facing documentation URL, echoed into both documents. */
60
+ documentationUri?: string;
61
+ }
62
+ /** Only S256. `plain` is not a PKCE method, it is the absence of one. */
63
+ export declare const CODE_CHALLENGE_METHODS_SUPPORTED: readonly ["S256"];
64
+ /** RFC 8628's grant type, named once so the metadata and the mount agree. */
65
+ export declare const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
66
+ /** What a boop app can meaningfully scope, before an operator narrows it. */
67
+ export declare const DEFAULT_SCOPES_SUPPORTED: string[];
68
+ /** One hour, matching the donor's `Cache-Control` and the ecosystem's habit. */
69
+ export declare const DISCOVERY_MAX_AGE_SECONDS = 3600;
70
+ export interface AuthorizationServerMetadata {
71
+ issuer: string;
72
+ authorization_endpoint: string;
73
+ token_endpoint: string;
74
+ registration_endpoint: string;
75
+ revocation_endpoint: string;
76
+ response_types_supported: string[];
77
+ grant_types_supported: string[];
78
+ code_challenge_methods_supported: string[];
79
+ token_endpoint_auth_methods_supported: string[];
80
+ scopes_supported: string[];
81
+ client_id_metadata_document_supported: boolean;
82
+ service_documentation?: string;
83
+ }
84
+ /** RFC 8414, plus the CIMD field the Claude ecosystem reads. */
85
+ export declare function buildAuthorizationServerMetadata(config: DiscoveryConfig): AuthorizationServerMetadata;
86
+ export interface ProtectedResourceMetadata {
87
+ resource: string;
88
+ authorization_servers: string[];
89
+ scopes_supported: string[];
90
+ bearer_methods_supported: string[];
91
+ resource_documentation?: string;
92
+ }
93
+ /** RFC 9728. */
94
+ export declare function buildProtectedResourceMetadata(config: DiscoveryConfig): ProtectedResourceMetadata;
95
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,yEAAyE;AACzE,eAAO,MAAM,gCAAgC,mBAAoB,CAAC;AAElE,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,iDAAiD,CAAC;AAErF,6EAA6E;AAC7E,eAAO,MAAM,wBAAwB,UAAyC,CAAC;AAE/E,gFAAgF;AAChF,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAO9C,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,MAAM,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gCAAgC,EAAE,MAAM,EAAE,CAAC;IAC3C,qCAAqC,EAAE,MAAM,EAAE,CAAC;IAChD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,qCAAqC,EAAE,OAAO,CAAC;IAC/C,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,gEAAgE;AAChE,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,eAAe,GACtB,2BAA2B,CAiC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,gBAAgB;AAChB,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,eAAe,GACtB,yBAAyB,CAe3B"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * The two `.well-known` discovery documents.
4
+ *
5
+ * ## Computed, never read off disk
6
+ *
7
+ * The donor controller (`paywalls-site/src/controllers/wellknownController.ts`)
8
+ * serves these by joining `req.params.resource` onto a fixed prefix and
9
+ * compiling whatever file it lands on. Two properties of that are wrong for a
10
+ * shipped package rather than for paywalls, and the findings name both:
11
+ *
12
+ * 1. `req.params.resource` reaches `path.join` without `..` being stripped.
13
+ * Paywalls is saved by there being nothing outside the prefix it would
14
+ * serve, which is a fact about its filesystem, not a property of its code.
15
+ * 2. The prefix is relative to the process working directory, which is fine
16
+ * for an application and fragile for a capability whose files may be
17
+ * scaffolded or may sit in `node_modules`.
18
+ *
19
+ * Neither applies here, because these documents are **computed from
20
+ * configuration** by the functions below. There is no path, so there is nothing
21
+ * to traverse, and there is no working directory to be wrong about.
22
+ *
23
+ * ## And why the values are all fresh
24
+ *
25
+ * The donor's documents do not survive at all. Its authorization-server
26
+ * document advertises `grant_types_supported: ["refresh_token"]` and nothing
27
+ * else, has no `code_challenge_methods_supported`, no
28
+ * `client_id_metadata_document_supported`, and points `authorization_endpoint`
29
+ * at a POST endpoint authenticated by an API key. Its protected-resource
30
+ * document sets `resource` to the bare host rather than to the protected
31
+ * endpoint URL, which is the one thing the Claude ecosystem checklist says must
32
+ * match exactly as clients enter it.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.DISCOVERY_MAX_AGE_SECONDS = exports.DEFAULT_SCOPES_SUPPORTED = exports.DEVICE_CODE_GRANT_TYPE = exports.CODE_CHALLENGE_METHODS_SUPPORTED = void 0;
36
+ exports.buildAuthorizationServerMetadata = buildAuthorizationServerMetadata;
37
+ exports.buildProtectedResourceMetadata = buildProtectedResourceMetadata;
38
+ /** Only S256. `plain` is not a PKCE method, it is the absence of one. */
39
+ exports.CODE_CHALLENGE_METHODS_SUPPORTED = ['S256'];
40
+ /** RFC 8628's grant type, named once so the metadata and the mount agree. */
41
+ exports.DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
42
+ /** What a boop app can meaningfully scope, before an operator narrows it. */
43
+ exports.DEFAULT_SCOPES_SUPPORTED = ['profile', 'email', 'offline_access'];
44
+ /** One hour, matching the donor's `Cache-Control` and the ecosystem's habit. */
45
+ exports.DISCOVERY_MAX_AGE_SECONDS = 3600;
46
+ /** Trim one trailing slash, so `https://app/` and `https://app` agree. */
47
+ function origin(value) {
48
+ return value.endsWith('/') ? value.slice(0, -1) : value;
49
+ }
50
+ /** RFC 8414, plus the CIMD field the Claude ecosystem reads. */
51
+ function buildAuthorizationServerMetadata(config) {
52
+ const base = origin(config.issuer);
53
+ const grantTypes = ['authorization_code', 'refresh_token'];
54
+ if (config.deviceCodeEnabled)
55
+ grantTypes.push(exports.DEVICE_CODE_GRANT_TYPE);
56
+ const metadata = {
57
+ issuer: base,
58
+ authorization_endpoint: `${base}/oauth/authorize`,
59
+ token_endpoint: `${base}/oauth/token`,
60
+ // Honest only because Slice 1 ships the endpoint. Advertising a
61
+ // registration endpoint that does not exist is the same class of lie as
62
+ // advertising the device grant with the flag off.
63
+ registration_endpoint: `${base}/oauth/register`,
64
+ // Honest for the same reason: `createRevocationRouter` mounts it on the
65
+ // same router the token endpoint is on. It was the one field in this
66
+ // document that broke the rule above, until it did.
67
+ revocation_endpoint: `${base}/oauth/revoke`,
68
+ response_types_supported: ['code'],
69
+ grant_types_supported: grantTypes,
70
+ // PKCE is not optional here. OAuth 2.1 requires it for public clients, and
71
+ // every client this server will see is public.
72
+ code_challenge_methods_supported: [...exports.CODE_CHALLENGE_METHODS_SUPPORTED],
73
+ // `none` is what a public client authenticates with: nothing. It has to be
74
+ // advertised, because a client that finds only confidential methods here
75
+ // concludes it cannot use this server at all.
76
+ token_endpoint_auth_methods_supported: ['none'],
77
+ scopes_supported: [...config.scopesSupported],
78
+ client_id_metadata_document_supported: true,
79
+ };
80
+ if (config.documentationUri)
81
+ metadata.service_documentation = config.documentationUri;
82
+ return metadata;
83
+ }
84
+ /** RFC 9728. */
85
+ function buildProtectedResourceMetadata(config) {
86
+ const metadata = {
87
+ // As clients enter it — see the field's note on DiscoveryConfig.
88
+ resource: origin(config.resource),
89
+ // One entry, and Claude reads only the first. A second entry would not be
90
+ // an error, but it would be decoration.
91
+ authorization_servers: [origin(config.issuer)],
92
+ scopes_supported: [...config.scopesSupported],
93
+ // The header, and only the header. A bearer token in a query string lands
94
+ // in access logs and `Referer`.
95
+ bearer_methods_supported: ['header'],
96
+ };
97
+ if (config.documentationUri)
98
+ metadata.resource_documentation = config.documentationUri;
99
+ return metadata;
100
+ }
101
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;;AAiEH,4EAmCC;AAWD,wEAiBC;AAhGD,yEAAyE;AAC5D,QAAA,gCAAgC,GAAG,CAAC,MAAM,CAAU,CAAC;AAElE,6EAA6E;AAChE,QAAA,sBAAsB,GAAG,8CAA8C,CAAC;AAErF,6EAA6E;AAChE,QAAA,wBAAwB,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC;AAE/E,gFAAgF;AACnE,QAAA,yBAAyB,GAAG,IAAI,CAAC;AAE9C,0EAA0E;AAC1E,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1D,CAAC;AAiBD,gEAAgE;AAChE,SAAgB,gCAAgC,CAC9C,MAAuB;IAEvB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEnC,MAAM,UAAU,GAAG,CAAC,oBAAoB,EAAE,eAAe,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,iBAAiB;QAAE,UAAU,CAAC,IAAI,CAAC,8BAAsB,CAAC,CAAC;IAEtE,MAAM,QAAQ,GAAgC;QAC5C,MAAM,EAAE,IAAI;QACZ,sBAAsB,EAAE,GAAG,IAAI,kBAAkB;QACjD,cAAc,EAAE,GAAG,IAAI,cAAc;QACrC,gEAAgE;QAChE,wEAAwE;QACxE,kDAAkD;QAClD,qBAAqB,EAAE,GAAG,IAAI,iBAAiB;QAC/C,wEAAwE;QACxE,qEAAqE;QACrE,oDAAoD;QACpD,mBAAmB,EAAE,GAAG,IAAI,eAAe;QAC3C,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,qBAAqB,EAAE,UAAU;QACjC,2EAA2E;QAC3E,+CAA+C;QAC/C,gCAAgC,EAAE,CAAC,GAAG,wCAAgC,CAAC;QACvE,2EAA2E;QAC3E,yEAAyE;QACzE,8CAA8C;QAC9C,qCAAqC,EAAE,CAAC,MAAM,CAAC;QAC/C,gBAAgB,EAAE,CAAC,GAAG,MAAM,CAAC,eAAe,CAAC;QAC7C,qCAAqC,EAAE,IAAI;KAC5C,CAAC;IAEF,IAAI,MAAM,CAAC,gBAAgB;QAAE,QAAQ,CAAC,qBAAqB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACtF,OAAO,QAAQ,CAAC;AAClB,CAAC;AAUD,gBAAgB;AAChB,SAAgB,8BAA8B,CAC5C,MAAuB;IAEvB,MAAM,QAAQ,GAA8B;QAC1C,iEAAiE;QACjE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;QACjC,0EAA0E;QAC1E,wCAAwC;QACxC,qBAAqB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,gBAAgB,EAAE,CAAC,GAAG,MAAM,CAAC,eAAe,CAAC;QAC7C,0EAA0E;QAC1E,gCAAgC;QAChC,wBAAwB,EAAE,CAAC,QAAQ,CAAC;KACrC,CAAC;IAEF,IAAI,MAAM,CAAC,gBAAgB;QAAE,QAAQ,CAAC,sBAAsB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACvF,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The registry's refusals, as typed errors carrying an RFC-shaped code.
3
+ *
4
+ * The code is what the controller writes into an OAuth error body, so it is
5
+ * chosen from the specification rather than invented: RFC 7591 §3.2.2 defines
6
+ * `invalid_client_metadata` and `invalid_redirect_uri` for registration, and
7
+ * RFC 6749 §5.2 defines `invalid_client` and `invalid_request`. Everything the
8
+ * specification has no word for gets a code prefixed `boop_`, so a reader can
9
+ * tell at a glance which half of the vocabulary they are looking at.
10
+ */
11
+ /** Codes a registration or lookup refusal can carry. */
12
+ export type ClientRegistryErrorCode = 'invalid_client_metadata' | 'invalid_redirect_uri' | 'invalid_client' | 'invalid_request'
13
+ /** The row exists and is `static`; the registration surface may not touch it. */
14
+ | 'boop_static_client_immutable'
15
+ /** The registry has reached its configured row ceiling. */
16
+ | 'boop_registration_capacity'
17
+ /** A Client ID Metadata Document could not be fetched, parsed or trusted. */
18
+ | 'boop_metadata_document_unusable';
19
+ export declare class ClientRegistryError extends Error {
20
+ readonly code: ClientRegistryErrorCode;
21
+ readonly status: number;
22
+ constructor(code: ClientRegistryErrorCode, message: string);
23
+ }
24
+ /** Is this one of ours? Narrower than `instanceof` across module instances. */
25
+ export declare function isClientRegistryError(err: unknown): err is ClientRegistryError;
26
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,wDAAwD;AACxD,MAAM,MAAM,uBAAuB,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,gBAAgB,GAChB,iBAAiB;AACnB,iFAAiF;GAC/E,8BAA8B;AAChC,2DAA2D;GACzD,4BAA4B;AAC9B,6EAA6E;GAC3E,iCAAiC,CAAC;AAatC,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM;CAM3D;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,mBAAmB,CAO9E"}
package/dist/errors.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * The registry's refusals, as typed errors carrying an RFC-shaped code.
4
+ *
5
+ * The code is what the controller writes into an OAuth error body, so it is
6
+ * chosen from the specification rather than invented: RFC 7591 §3.2.2 defines
7
+ * `invalid_client_metadata` and `invalid_redirect_uri` for registration, and
8
+ * RFC 6749 §5.2 defines `invalid_client` and `invalid_request`. Everything the
9
+ * specification has no word for gets a code prefixed `boop_`, so a reader can
10
+ * tell at a glance which half of the vocabulary they are looking at.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ClientRegistryError = void 0;
14
+ exports.isClientRegistryError = isClientRegistryError;
15
+ /** HTTP status a controller should answer a given code with. */
16
+ const STATUS_BY_CODE = {
17
+ invalid_client_metadata: 400,
18
+ invalid_redirect_uri: 400,
19
+ invalid_client: 400,
20
+ invalid_request: 400,
21
+ boop_static_client_immutable: 403,
22
+ boop_registration_capacity: 503,
23
+ boop_metadata_document_unusable: 400,
24
+ };
25
+ class ClientRegistryError extends Error {
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.name = 'ClientRegistryError';
29
+ this.code = code;
30
+ this.status = STATUS_BY_CODE[code];
31
+ }
32
+ }
33
+ exports.ClientRegistryError = ClientRegistryError;
34
+ /** Is this one of ours? Narrower than `instanceof` across module instances. */
35
+ function isClientRegistryError(err) {
36
+ return (typeof err === 'object' &&
37
+ err !== null &&
38
+ err.name === 'ClientRegistryError' &&
39
+ typeof err.code === 'string');
40
+ }
41
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAuCH,sDAOC;AA/BD,gEAAgE;AAChE,MAAM,cAAc,GAA4C;IAC9D,uBAAuB,EAAE,GAAG;IAC5B,oBAAoB,EAAE,GAAG;IACzB,cAAc,EAAE,GAAG;IACnB,eAAe,EAAE,GAAG;IACpB,4BAA4B,EAAE,GAAG;IACjC,0BAA0B,EAAE,GAAG;IAC/B,+BAA+B,EAAE,GAAG;CACrC,CAAC;AAEF,MAAa,mBAAoB,SAAQ,KAAK;IAI5C,YAAY,IAA6B,EAAE,OAAe;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;CACF;AAVD,kDAUC;AAED,+EAA+E;AAC/E,SAAgB,qBAAqB,CAAC,GAAY;IAChD,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAA0B,CAAC,IAAI,KAAK,qBAAqB;QAC1D,OAAQ,GAA0B,CAAC,IAAI,KAAK,QAAQ,CACrD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { OAuthGrantRecord } from './grant-service.js';
2
+ /**
3
+ * The Express request fields this capability reads and writes.
4
+ *
5
+ * Declared here rather than in the file that writes them so that both the
6
+ * consent controller (which reads `req.user`) and the bearer middleware (which
7
+ * writes `req.oauthGrant`) get the same declarations without importing each
8
+ * other, and so a consuming application picks them up by importing the package.
9
+ *
10
+ * ## Why each of these is a *re*-declaration, and why that is safe
11
+ *
12
+ * cap-users declares `user`, `sessionToken` and `authStrategy` in its
13
+ * scaffolded `src/types/express.ts`, which exists only inside a generated
14
+ * application — this package has to type-check without one. Global interface
15
+ * declarations merge, so both files can describe the same field as long as they
16
+ * describe it **identically**; TypeScript reports TS2717 the moment two
17
+ * declarations of one property disagree about its type.
18
+ *
19
+ * So: `authStrategy` is `string` here, exactly as cap-users widened it in Slice
20
+ * 0 — not the `'api-key'` literal it used to be, which is the whole point of
21
+ * the widening. And `user` hangs off `Express.User` rather than naming a
22
+ * concrete type, for the reason cap-users' own file explains at length:
23
+ * `@types/passport` types `Request['user']` as `Express.User`, so any capability
24
+ * that named a different type would collide with it and break every handler in
25
+ * the application. Extending the empty interface is what lets three packages
26
+ * describe one field and still agree.
27
+ */
28
+ declare global {
29
+ namespace Express {
30
+ interface User {
31
+ }
32
+ interface Request {
33
+ /** Set by cap-users' cookie resolver or by this package's bearer path. */
34
+ user?: User;
35
+ /** The raw credential this request presented. cap-users' field. */
36
+ sessionToken?: string;
37
+ /**
38
+ * Which door this request came through. `'oauth'` for this package's
39
+ * bearer path — never `'api-key'`, which is the literal `requireApiKey`
40
+ * gates on, and which an OAuth access token must fail by construction.
41
+ */
42
+ authStrategy?: string;
43
+ /**
44
+ * The grant behind this request's OAuth access token — written by
45
+ * `createOAuthBearerMiddleware` and by nothing else.
46
+ *
47
+ * It is the **scope authority**: the token carries no scope copy, so a
48
+ * grant narrowed a moment ago binds this request. Present only when
49
+ * `authStrategy === 'oauth'`.
50
+ */
51
+ oauthGrant?: OAuthGrantRecord;
52
+ /**
53
+ * The `WWW-Authenticate` challenge this deployment sends on a 401 —
54
+ * written by `createOAuthBearerMiddleware` and read by `requireOAuth`.
55
+ *
56
+ * It exists so the resolver and the gate cannot be configured with
57
+ * different challenges. Set on every request the resolver sees, including
58
+ * the ones it lets through unauthenticated, because those are exactly the
59
+ * ones the gate has to refuse.
60
+ */
61
+ oauthChallenge?: string;
62
+ }
63
+ }
64
+ }
65
+ export {};
66
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAKhB,UAAU,IAAI;SAAG;QAEjB,UAAU,OAAO;YACf,0EAA0E;YAC1E,IAAI,CAAC,EAAE,IAAI,CAAC;YACZ,mEAAmE;YACnE,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB;;;;eAIG;YACH,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB;;;;;;;eAOG;YACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;YAC9B;;;;;;;;eAQG;YACH,cAAc,CAAC,EAAE,MAAM,CAAC;SACzB;KACF;CACF;AAED,OAAO,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ import type { PropertyStore } from './user-store.js';
2
+ /**
3
+ * `oauth-grants` — the record of what a person approved, for which client.
4
+ *
5
+ * ## The grant is the scope authority, and the token carries no copy
6
+ *
7
+ * The spec says a minted token "inherits" the code's approved scopes, and
8
+ * separately that the resource server reads scopes from `oauth-grants`. If both
9
+ * were true there would be two answers and they would eventually disagree. The
10
+ * plan settles it: the code's `scopes` are written **into the grant** at
11
+ * redemption, and the guard reads the grant on every request. A token minted
12
+ * under a wider set is bound by the grant's current, narrower one on its very
13
+ * next call — no revocation, no new mechanism, and a test says so.
14
+ *
15
+ * RFC 8707 `resource` is the exception and stays on the token: an audience is a
16
+ * property of a credential, not of a relationship, which is exactly why a
17
+ * cautious client can ask for a narrow token against an unchanged grant.
18
+ *
19
+ * ## Re-consent replaces, it does not union
20
+ *
21
+ * The screen showed the user a list and they approved that list. A union would
22
+ * grant scopes the screen never showed, which is the one thing a consent screen
23
+ * must never do. Replacement also gives a downgrade path, which a union has no
24
+ * way to express.
25
+ *
26
+ * ## Nothing in here is a credential
27
+ *
28
+ * Only `credentials` is on cap-users' sensitive-property list, so this property
29
+ * is readable by default. Client ids, scopes and timestamps belong here. Token
30
+ * values, raw codes and PKCE verifiers do not, and stay in identity rows as
31
+ * hashes. `refresh_key` is the one field that looks like an exception and is
32
+ * not: it is the *stored* identity key, which is already a SHA-256 digest, and
33
+ * it is the same value `user_identities.key` holds. The credentials-at-rest
34
+ * test asserts against the raw values, and this passes it.
35
+ */
36
+ /** The property name. One place, because two spellings is one bug. */
37
+ export declare const OAUTH_GRANTS_PROPERTY = "oauth-grants";
38
+ /** Tags the blob's shape, the way cap-users tags its own property schemas. */
39
+ export declare const OAUTH_GRANTS_INFO_SCHEMA = "boop.oauth-server.grants.v1";
40
+ export interface OAuthGrantRecord {
41
+ /** Stable, non-secret handle. What the connected-clients screen links to. */
42
+ grant_id: string;
43
+ /** What the client presents as `client_id` — a URL, for a CIMD client. */
44
+ client_id: string;
45
+ /** The token prefix. The join between a presented token and this record. */
46
+ client_public_id: string;
47
+ /** The current approved set. Replaced wholesale on re-consent. */
48
+ scopes: string[];
49
+ /** RFC 8707, as last requested. Informational here; the token is authority. */
50
+ resource: string | null;
51
+ /**
52
+ * The identity key of the grant's **current** refresh token, or `null`.
53
+ *
54
+ * This is what makes reuse of a rotated refresh token detectable. A rotated
55
+ * token's identity row is deliberately left *active* so that presenting it
56
+ * still resolves to a user and therefore to this grant; the row grants
57
+ * nothing, because the key no longer matches this field. See
58
+ * `token-controller.ts` for the whole argument.
59
+ */
60
+ refresh_key: string | null;
61
+ created_at: string;
62
+ updated_at: string;
63
+ /** Soft revocation. A grant is never deleted; the history is the point. */
64
+ ended_at: string | null;
65
+ ended_reason: string | null;
66
+ }
67
+ export interface ApproveGrantInput {
68
+ clientId: string;
69
+ clientPublicId: string;
70
+ scopes: string[];
71
+ resource?: string | null;
72
+ }
73
+ export interface GrantsSnapshot {
74
+ grants: OAuthGrantRecord[];
75
+ revision: number | undefined;
76
+ }
77
+ export interface GrantService {
78
+ /** The whole property, with the revision a write has to quote back. */
79
+ read(userId: number): Promise<GrantsSnapshot>;
80
+ /** Write the whole array under `expectedRevision`. Refused if stale. */
81
+ write(userId: number, grants: OAuthGrantRecord[], revision: number | undefined): Promise<void>;
82
+ /** Every grant, ended ones included — the connected-clients screen's read. */
83
+ list(userId: number): Promise<OAuthGrantRecord[]>;
84
+ /** The live grant for this client, if there is one. */
85
+ findActive(userId: number, clientPublicId: string): Promise<OAuthGrantRecord | null>;
86
+ findById(userId: number, grantId: string): Promise<OAuthGrantRecord | null>;
87
+ /** Create or re-consent. Replaces the scope set; never unions it. */
88
+ approve(userId: number, input: ApproveGrantInput): Promise<OAuthGrantRecord>;
89
+ /** Record the rotation. The old key stops being the grant's current one. */
90
+ setRefreshKey(userId: number, grantId: string, refreshKey: string | null): Promise<void>;
91
+ /** Soft revocation, with the reason that will be read back months later. */
92
+ end(userId: number, grantId: string, reason: string): Promise<OAuthGrantRecord | null>;
93
+ }
94
+ export interface GrantServiceOptions {
95
+ propertyStore: PropertyStore;
96
+ /** Injectable clock, so a test can pin `created_at`. */
97
+ now?: () => Date;
98
+ }
99
+ /** Is this grant live? One predicate, so the guard and the screen agree. */
100
+ export declare function isGrantActive(grant: OAuthGrantRecord): boolean;
101
+ export declare function createGrantService(options: GrantServiceOptions): GrantService;
102
+ //# sourceMappingURL=grant-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-service.d.ts","sourceRoot":"","sources":["../src/grant-service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AAEpD,8EAA8E;AAC9E,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;;;;OAQG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC9C,wEAAwE;IACxE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/F,8EAA8E;IAC9E,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAClD,uDAAuD;IACvD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC5E,qEAAqE;IACrE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC7E,4EAA4E;IAC5E,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,4EAA4E;IAC5E,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACxF;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,aAAa,CAAC;IAC7B,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAE9D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAoH7E"}
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAUTH_GRANTS_INFO_SCHEMA = exports.OAUTH_GRANTS_PROPERTY = void 0;
4
+ exports.isGrantActive = isGrantActive;
5
+ exports.createGrantService = createGrantService;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const property_array_js_1 = require("./property-array.js");
8
+ /**
9
+ * `oauth-grants` — the record of what a person approved, for which client.
10
+ *
11
+ * ## The grant is the scope authority, and the token carries no copy
12
+ *
13
+ * The spec says a minted token "inherits" the code's approved scopes, and
14
+ * separately that the resource server reads scopes from `oauth-grants`. If both
15
+ * were true there would be two answers and they would eventually disagree. The
16
+ * plan settles it: the code's `scopes` are written **into the grant** at
17
+ * redemption, and the guard reads the grant on every request. A token minted
18
+ * under a wider set is bound by the grant's current, narrower one on its very
19
+ * next call — no revocation, no new mechanism, and a test says so.
20
+ *
21
+ * RFC 8707 `resource` is the exception and stays on the token: an audience is a
22
+ * property of a credential, not of a relationship, which is exactly why a
23
+ * cautious client can ask for a narrow token against an unchanged grant.
24
+ *
25
+ * ## Re-consent replaces, it does not union
26
+ *
27
+ * The screen showed the user a list and they approved that list. A union would
28
+ * grant scopes the screen never showed, which is the one thing a consent screen
29
+ * must never do. Replacement also gives a downgrade path, which a union has no
30
+ * way to express.
31
+ *
32
+ * ## Nothing in here is a credential
33
+ *
34
+ * Only `credentials` is on cap-users' sensitive-property list, so this property
35
+ * is readable by default. Client ids, scopes and timestamps belong here. Token
36
+ * values, raw codes and PKCE verifiers do not, and stay in identity rows as
37
+ * hashes. `refresh_key` is the one field that looks like an exception and is
38
+ * not: it is the *stored* identity key, which is already a SHA-256 digest, and
39
+ * it is the same value `user_identities.key` holds. The credentials-at-rest
40
+ * test asserts against the raw values, and this passes it.
41
+ */
42
+ /** The property name. One place, because two spellings is one bug. */
43
+ exports.OAUTH_GRANTS_PROPERTY = 'oauth-grants';
44
+ /** Tags the blob's shape, the way cap-users tags its own property schemas. */
45
+ exports.OAUTH_GRANTS_INFO_SCHEMA = 'boop.oauth-server.grants.v1';
46
+ /** Is this grant live? One predicate, so the guard and the screen agree. */
47
+ function isGrantActive(grant) {
48
+ return grant.ended_at === null;
49
+ }
50
+ function createGrantService(options) {
51
+ const { propertyStore } = options;
52
+ const now = options.now ?? (() => new Date());
53
+ async function read(userId) {
54
+ const snapshot = await (0, property_array_js_1.readArray)(propertyStore, userId, exports.OAUTH_GRANTS_PROPERTY);
55
+ return { grants: snapshot.entries, revision: snapshot.revision };
56
+ }
57
+ async function write(userId, grants, revision) {
58
+ await (0, property_array_js_1.writeArray)(propertyStore, userId, exports.OAUTH_GRANTS_PROPERTY, grants, { revision }, {
59
+ infoSchema: exports.OAUTH_GRANTS_INFO_SCHEMA,
60
+ });
61
+ }
62
+ return {
63
+ read,
64
+ write,
65
+ async list(userId) {
66
+ return (await read(userId)).grants;
67
+ },
68
+ async findActive(userId, clientPublicId) {
69
+ const { grants } = await read(userId);
70
+ return (grants.find((g) => g.client_public_id === clientPublicId && isGrantActive(g)) ?? null);
71
+ },
72
+ async findById(userId, grantId) {
73
+ const { grants } = await read(userId);
74
+ return grants.find((g) => g.grant_id === grantId) ?? null;
75
+ },
76
+ async approve(userId, input) {
77
+ const { grants, revision } = await read(userId);
78
+ const timestamp = now().toISOString();
79
+ const index = grants.findIndex((g) => g.client_public_id === input.clientPublicId && isGrantActive(g));
80
+ // Re-consent REPLACES the scope set. See the header: a union would grant
81
+ // scopes the screen the user just read never showed them.
82
+ const grant = index >= 0
83
+ ? {
84
+ ...grants[index],
85
+ client_id: input.clientId,
86
+ scopes: [...input.scopes],
87
+ resource: input.resource ?? null,
88
+ updated_at: timestamp,
89
+ }
90
+ : {
91
+ grant_id: (0, node_crypto_1.randomUUID)(),
92
+ client_id: input.clientId,
93
+ client_public_id: input.clientPublicId,
94
+ scopes: [...input.scopes],
95
+ resource: input.resource ?? null,
96
+ refresh_key: null,
97
+ created_at: timestamp,
98
+ updated_at: timestamp,
99
+ ended_at: null,
100
+ ended_reason: null,
101
+ };
102
+ const next = index >= 0 ? grants.map((g, i) => (i === index ? grant : g)) : [...grants, grant];
103
+ await write(userId, next, revision);
104
+ return grant;
105
+ },
106
+ async setRefreshKey(userId, grantId, refreshKey) {
107
+ const { grants, revision } = await read(userId);
108
+ const timestamp = now().toISOString();
109
+ const next = grants.map((g) => g.grant_id === grantId ? { ...g, refresh_key: refreshKey, updated_at: timestamp } : g);
110
+ await write(userId, next, revision);
111
+ },
112
+ async end(userId, grantId, reason) {
113
+ const { grants, revision } = await read(userId);
114
+ const target = grants.find((g) => g.grant_id === grantId);
115
+ if (!target)
116
+ return null;
117
+ // Already ended? Keep the first reason and the first timestamp: the
118
+ // interesting fact is why it ended, and a second revoke overwriting
119
+ // "refresh_reuse" with "user_revoked" would erase the theft signal.
120
+ if (!isGrantActive(target))
121
+ return target;
122
+ const timestamp = now().toISOString();
123
+ const ended = {
124
+ ...target,
125
+ // A revoked grant's refresh token is gone, and leaving the key here
126
+ // would make a later reuse read as a rotation rather than as reuse.
127
+ refresh_key: null,
128
+ ended_at: timestamp,
129
+ ended_reason: reason,
130
+ updated_at: timestamp,
131
+ };
132
+ await write(userId, grants.map((g) => (g.grant_id === grantId ? ended : g)), revision);
133
+ return ended;
134
+ },
135
+ };
136
+ }
137
+ //# sourceMappingURL=grant-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-service.js","sourceRoot":"","sources":["../src/grant-service.ts"],"names":[],"mappings":"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"}