@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
package/dist/bearer.js ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_BEARER_CHALLENGE = void 0;
4
+ exports.createOAuthTokenResolver = createOAuthTokenResolver;
5
+ exports.createOAuthBearerMiddleware = createOAuthBearerMiddleware;
6
+ exports.missingScopes = missingScopes;
7
+ exports.requireOAuth = requireOAuth;
8
+ exports.createOAuthResourceServer = createOAuthResourceServer;
9
+ const cap_users_1 = require("@boopkit/cap-users");
10
+ require("./express.js");
11
+ const token_service_js_1 = require("./token-service.js");
12
+ /**
13
+ * The lookup itself, exported so it can be tested without a request.
14
+ *
15
+ * Returns `null` for every reason a token is not usable — malformed, unknown,
16
+ * revoked, expired, or belonging to a grant that has been ended. The caller
17
+ * cannot tell them apart, and should not: the difference is only interesting to
18
+ * whoever stole the token.
19
+ */
20
+ function createOAuthTokenResolver(options) {
21
+ const { tokenService, grantService, findUserById } = options;
22
+ return async function resolveOAuthToken(token) {
23
+ const resolved = await tokenService.resolve(token_service_js_1.OAUTH_ACCESS_DOMAIN, token);
24
+ if (!resolved)
25
+ return null;
26
+ const grant = await grantService.findActive(resolved.userId, resolved.clientPublicId);
27
+ if (!grant)
28
+ return null;
29
+ const user = await findUserById(resolved.userId);
30
+ if (!user)
31
+ return null;
32
+ return { user, grant, clientPublicId: resolved.clientPublicId };
33
+ };
34
+ }
35
+ /**
36
+ * The middleware an application mounts.
37
+ *
38
+ * cap-users' factory takes the lookup as a plain callback with no request in
39
+ * its signature — deliberately, since resolving a credential has no business
40
+ * reading one. But this capability has to hand the resolved grant to the
41
+ * handlers downstream, and the closure is the only place the two can meet, so
42
+ * the binding is made per request. That is a closure allocation, not work; the
43
+ * three reads inside it are the cost of the request either way.
44
+ */
45
+ function createOAuthBearerMiddleware(deps, options) {
46
+ const resolveOAuthToken = createOAuthTokenResolver(options);
47
+ const challenge = options.challenge ?? 'Bearer';
48
+ return function oauthBearer(req, res, next) {
49
+ // Hand the challenge forward to whatever gate runs behind this. Written
50
+ // unconditionally, before any lookup, because the gate's 401 is for the
51
+ // caller who presented nothing at all — the path on which the resolver
52
+ // itself never refuses anybody.
53
+ req.oauthChallenge = challenge;
54
+ const middleware = (0, cap_users_1.createBearerMiddleware)(deps, {
55
+ strategy: 'oauth',
56
+ challenge,
57
+ resolve: async (token) => {
58
+ const resolved = await resolveOAuthToken(token);
59
+ if (!resolved)
60
+ return null;
61
+ req.oauthGrant = resolved.grant;
62
+ return { user: resolved.user };
63
+ },
64
+ });
65
+ middleware(req, res, next);
66
+ };
67
+ }
68
+ /** The challenge sent when the request carried no usable OAuth token at all. */
69
+ exports.DEFAULT_BEARER_CHALLENGE = 'Bearer';
70
+ /**
71
+ * Which of the required scopes this grant does not cover.
72
+ *
73
+ * Set membership, so the comparison does not depend on the order either list
74
+ * happens to be in — a grant approved as `email profile` covers a route that
75
+ * asks for `profile email`. Duplicates in the requirement collapse, so the
76
+ * answer is stable enough to put in a header.
77
+ */
78
+ function missingScopes(granted, required) {
79
+ const held = new Set(granted);
80
+ return [...new Set(required)].filter((scope) => !held.has(scope));
81
+ }
82
+ /**
83
+ * Quote a value we are about to place inside a `WWW-Authenticate` parameter.
84
+ *
85
+ * Scope names reach here from a route definition rather than from a request, so
86
+ * this is not defending against an attacker. It is defending against a header
87
+ * that silently stops parsing at a stray quote, which is the kind of fault that
88
+ * shows up as "the client ignores our error" three weeks later.
89
+ */
90
+ function headerSafe(value) {
91
+ return value.replace(/[\\"]/g, '');
92
+ }
93
+ /**
94
+ * `requireOAuth(...scopes)` — the resource-server gate.
95
+ *
96
+ * The request must have arrived on an OAuth access token backed by a live
97
+ * grant, and that grant must cover every named scope. Both refusals are RFC
98
+ * 6750:
99
+ *
100
+ * - **401** with `WWW-Authenticate: <challenge>` when there is no usable
101
+ * token. The challenge is the resolver's — see the file header — so it
102
+ * carries RFC 9728's `resource_metadata` pointer and an unauthenticated
103
+ * caller can find its way to the document that says where to go. Claude
104
+ * does not honour that header on a 200, so the 401 is the only place it can
105
+ * arrive.
106
+ * - **403** with `error="insufficient_scope"` and the required `scope`, in
107
+ * the header per §3.1 and in the body so a client that reads JSON errors
108
+ * rather than headers gets the same answer.
109
+ *
110
+ * The **grant** is the authority, not the token: a token minted under a wider
111
+ * consent is bound by a narrowed grant on its very next request, with no
112
+ * revocation involved.
113
+ *
114
+ * Mount it behind `resolveBearer`. `createOAuthResourceServer` pairs the two so
115
+ * an application cannot mount one without the other.
116
+ */
117
+ function requireOAuth(...requiredScopes) {
118
+ const required = [...new Set(requiredScopes)];
119
+ return function oauthGate(req, res, next) {
120
+ const challenge = req.oauthChallenge ?? exports.DEFAULT_BEARER_CHALLENGE;
121
+ const grant = req.oauthGrant;
122
+ if (req.authStrategy !== 'oauth' || !grant) {
123
+ res.set('WWW-Authenticate', challenge).status(401).json({
124
+ error: 'invalid_token',
125
+ error_description: 'a valid OAuth access token is required',
126
+ });
127
+ return;
128
+ }
129
+ const missing = missingScopes(grant.scopes, required);
130
+ if (missing.length > 0) {
131
+ const scope = required.join(' ');
132
+ const description = `this connection was not approved for: ${missing.join(' ')}`;
133
+ res
134
+ .set('WWW-Authenticate', `${challenge}, error="insufficient_scope", ` +
135
+ `error_description="${headerSafe(description)}", scope="${headerSafe(scope)}"`)
136
+ .status(403)
137
+ .json({ error: 'insufficient_scope', error_description: description, scope });
138
+ return;
139
+ }
140
+ next();
141
+ };
142
+ }
143
+ function createOAuthResourceServer(deps, options) {
144
+ const challenge = options.challenge ?? exports.DEFAULT_BEARER_CHALLENGE;
145
+ return {
146
+ resolveBearer: createOAuthBearerMiddleware(deps, { ...options, challenge }),
147
+ requireOAuth,
148
+ };
149
+ }
150
+ //# sourceMappingURL=bearer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Reading one string field off a body that arrived as a form or as JSON.
3
+ *
4
+ * ## Why this is a module rather than a helper in each controller
5
+ *
6
+ * Two endpoints — `POST /oauth/token` and `POST /oauth/revoke` — mount both
7
+ * body parsers and accept either encoding, for the reason the token
8
+ * controller's header gives: the specification says form, and real clients send
9
+ * JSON. Both therefore need the same answer to one awkward question, which is
10
+ * what counts as *present*.
11
+ *
12
+ * That answer is not obvious and it is not the same as truthiness. A form body
13
+ * turns an omitted field into `""` rather than into `undefined`, and Express
14
+ * hands back an **array** when a field is repeated — `token=a&token=b` arrives
15
+ * as `['a', 'b']`, which a naive read would pass along to a database query as
16
+ * an object. So: only a `string` counts, it is trimmed, and an empty result is
17
+ * absence. A second copy of that rule is a second place that has to be right
18
+ * about it, and the copy that drifts is the one that lets an array through.
19
+ */
20
+ /** A single string off a body Express may have handed us an array for. */
21
+ export declare function field(body: unknown, name: string): string | undefined;
22
+ /** The same, refusing with RFC 6749 §5.2's `invalid_request` when absent. */
23
+ export declare function required(body: unknown, name: string): string;
24
+ //# sourceMappingURL=body-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-field.d.ts","sourceRoot":"","sources":["../src/body-field.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.field = field;
4
+ exports.required = required;
5
+ const oauth_error_js_1 = require("./oauth-error.js");
6
+ /**
7
+ * Reading one string field off a body that arrived as a form or as JSON.
8
+ *
9
+ * ## Why this is a module rather than a helper in each controller
10
+ *
11
+ * Two endpoints — `POST /oauth/token` and `POST /oauth/revoke` — mount both
12
+ * body parsers and accept either encoding, for the reason the token
13
+ * controller's header gives: the specification says form, and real clients send
14
+ * JSON. Both therefore need the same answer to one awkward question, which is
15
+ * what counts as *present*.
16
+ *
17
+ * That answer is not obvious and it is not the same as truthiness. A form body
18
+ * turns an omitted field into `""` rather than into `undefined`, and Express
19
+ * hands back an **array** when a field is repeated — `token=a&token=b` arrives
20
+ * as `['a', 'b']`, which a naive read would pass along to a database query as
21
+ * an object. So: only a `string` counts, it is trimmed, and an empty result is
22
+ * absence. A second copy of that rule is a second place that has to be right
23
+ * about it, and the copy that drifts is the one that lets an array through.
24
+ */
25
+ /** A single string off a body Express may have handed us an array for. */
26
+ function field(body, name) {
27
+ if (typeof body !== 'object' || body === null)
28
+ return undefined;
29
+ const value = body[name];
30
+ if (typeof value !== 'string')
31
+ return undefined;
32
+ const trimmed = value.trim();
33
+ return trimmed === '' ? undefined : trimmed;
34
+ }
35
+ /** The same, refusing with RFC 6749 §5.2's `invalid_request` when absent. */
36
+ function required(body, name) {
37
+ const value = field(body, name);
38
+ if (value === undefined) {
39
+ throw new oauth_error_js_1.OAuthError('invalid_request', `${name} is required`);
40
+ }
41
+ return value;
42
+ }
43
+ //# sourceMappingURL=body-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-field.js","sourceRoot":"","sources":["../src/body-field.ts"],"names":[],"mappings":";;AAuBA,sBAMC;AAGD,4BAMC;AAtCD,qDAA8C;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,SAAgB,KAAK,CAAC,IAAa,EAAE,IAAY;IAC/C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,KAAK,GAAI,IAAgC,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED,6EAA6E;AAC7E,SAAgB,QAAQ,CAAC,IAAa,EAAE,IAAY;IAClD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,2BAAU,CAAC,iBAAiB,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,120 @@
1
+ import { type FetchLike } from './metadata-document.js';
2
+ import type { ClientMetadataInput, OauthClient, OauthClientDelegate, StaticClientInput } from './types.js';
3
+ /**
4
+ * The client registry — who is asking, and what they are allowed to ask for.
5
+ *
6
+ * One table, one lookup path, three registration modes. A mode says where a
7
+ * client's *authoritative* metadata lives, and almost nothing else:
8
+ *
9
+ * `dynamic` open RFC 7591 registration; we own the metadata
10
+ * `metadata_document` the `client_id` is an https URL; the client owns it
11
+ * `static` an operator wrote the row; only an operator changes it
12
+ *
13
+ * ## What is not here
14
+ *
15
+ * Nothing user- or account-shaped. A client is software; the record tying a
16
+ * client to a *person* is the grant, which lives in cap-users' user properties
17
+ * and belongs to Slice 2. The registry never learns who consented.
18
+ *
19
+ * ## The abuse controls, and why they are fresh work
20
+ *
21
+ * Neither donor has an unauthenticated registration endpoint. Paywalls'
22
+ * `/oauth/register` requires a caller of user type `service-user` and creates a
23
+ * row owned by that caller's account; open registration has no caller and no
24
+ * account, so the gate that made paywalls' endpoint safe has nothing to map
25
+ * onto. What replaces it is here and in `createRegistrationRateLimiter`:
26
+ * `redirect_uris` required and validated at registration, a ceiling on total
27
+ * rows, per-IP rate limiting at the transport, and a prune for rows that never
28
+ * went anywhere.
29
+ */
30
+ /** Sensible RFC 7591 defaults for a client that asked for nothing specific. */
31
+ export declare const DEFAULT_GRANT_TYPES: string[];
32
+ export declare const DEFAULT_RESPONSE_TYPES: string[];
33
+ /** 24 hours. A cached metadata document older than this is re-fetched lazily. */
34
+ export declare const DEFAULT_METADATA_MAX_AGE_MS: number;
35
+ /** 30 days, per the plan: a dynamic row that never went anywhere is garbage. */
36
+ export declare const DEFAULT_ABANDONED_CLIENT_TTL_MS: number;
37
+ /** How many rows open registration may create before it starts refusing. */
38
+ export declare const DEFAULT_MAX_REGISTERED_CLIENTS = 1000;
39
+ export interface ClientRegistryServiceOptions {
40
+ /**
41
+ * The HTTP boundary for metadata documents. Required only if
42
+ * `metadata_document` clients are in play; a registry that never sees one
43
+ * never calls it.
44
+ */
45
+ fetch?: FetchLike;
46
+ /** Ceiling on `dynamic` rows. `Infinity` disables the check. */
47
+ maxRegisteredClients?: number;
48
+ /** Cache age past which a metadata document is re-fetched. */
49
+ metadataMaxAgeMs?: number;
50
+ metadataTimeoutMs?: number;
51
+ metadataMaxBytes?: number;
52
+ metadataMaxRedirects?: number;
53
+ /** Scopes a client may be granted, if it named none. */
54
+ defaultScopes?: string[];
55
+ /** Injectable clock, so expiry and cache-age tests do not sleep. */
56
+ now?: () => Date;
57
+ /** Called when a re-fetch fails and the cached copy is used instead. */
58
+ onMetadataRefreshFailed?(clientId: string, err: unknown): void;
59
+ }
60
+ export interface PruneOptions {
61
+ /** Delete abandoned `dynamic` rows created before this instant. */
62
+ before?: Date;
63
+ /**
64
+ * Is this row still in use?
65
+ *
66
+ * Slice 1 has no grants, so the default answer is "no" and the prune is safe
67
+ * to run only against a registry whose clients have not connected yet.
68
+ * Slice 3 owns grants and injects the real predicate; until it does, the
69
+ * scaffold does not schedule this.
70
+ */
71
+ isInUse?(client: OauthClient): Promise<boolean>;
72
+ }
73
+ export interface ClientRegistryService {
74
+ /** RFC 7591 open registration. Generates the `publicId` and the `client_id`. */
75
+ registerDynamic(input: ClientMetadataInput): Promise<OauthClient>;
76
+ /**
77
+ * Resolve a Client ID Metadata Document client, fetching and caching the
78
+ * document on first sight and lazily re-fetching a stale copy.
79
+ */
80
+ resolveMetadataDocument(clientId: string): Promise<OauthClient>;
81
+ /** Operator-configured client. Idempotent, so a seed script may re-run. */
82
+ registerStaticClient(input: StaticClientInput): Promise<OauthClient>;
83
+ /**
84
+ * The authorization endpoint's lookup: a raw `client_id` string off a query
85
+ * parameter, which for a CIMD client is a URL.
86
+ */
87
+ findByClientId(clientId: string): Promise<OauthClient | null>;
88
+ /** Lookup by the token prefix, for the resource-server path. */
89
+ findByPublicId(publicId: string): Promise<OauthClient | null>;
90
+ /** Suspend without deleting a row that live tokens carry the prefix of. */
91
+ suspend(clientId: string): Promise<OauthClient>;
92
+ /** Re-enable a suspended client. */
93
+ unsuspend(clientId: string): Promise<OauthClient>;
94
+ /** Registration-surface update. Refuses a `static` row. */
95
+ updateRegistration(clientId: string, input: ClientMetadataInput): Promise<OauthClient>;
96
+ /** Registration-surface delete. Refuses a `static` row. */
97
+ deleteRegistration(clientId: string): Promise<void>;
98
+ /** How many rows the ceiling counts. */
99
+ countRegistered(): Promise<number>;
100
+ /** Garbage-collect abandoned dynamic rows. Returns the client ids removed. */
101
+ pruneAbandonedClients(options?: PruneOptions): Promise<string[]>;
102
+ }
103
+ /**
104
+ * The redirect-URI rule at *registration*, which is the other half of
105
+ * `matchRedirectUri`. The matcher decides whether a presented URI satisfies a
106
+ * stored one; this decides whether a URI may be stored at all.
107
+ *
108
+ * Both halves read `classifyRedirectUri`, and that is the point: a URI this
109
+ * function stores is by construction a URI the matcher will compare. The three
110
+ * shapes it admits — claimed https, loopback http, and an application's own
111
+ * private-use scheme — and the grounds each is admitted or refused on live in
112
+ * `redirect-uri.ts` next to the matching rule they have to agree with.
113
+ *
114
+ * This is the umbrella spec's "declared-but-unenforced redirect URI lists"
115
+ * defect closed at the point of entry: a list that was never validated going in
116
+ * cannot be enforced coming out, whatever the matcher does.
117
+ */
118
+ export declare function validateRedirectUris(value: unknown): string[];
119
+ export declare function createClientRegistryService(model: OauthClientDelegate, options?: ClientRegistryServiceOptions): ClientRegistryService;
120
+ //# sourceMappingURL=client-registry-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-registry-service.d.ts","sourceRoot":"","sources":["../src/client-registry-service.ts"],"names":[],"mappings":"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"}