@c9up/visa 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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/VisaManager.d.ts +94 -0
- package/dist/VisaManager.d.ts.map +1 -0
- package/dist/VisaManager.js +216 -0
- package/dist/VisaManager.js.map +1 -0
- package/dist/VisaProvider.d.ts +41 -0
- package/dist/VisaProvider.d.ts.map +1 -0
- package/dist/VisaProvider.js +153 -0
- package/dist/VisaProvider.js.map +1 -0
- package/dist/augmentations.d.ts +25 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +19 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/authorize.d.ts +78 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +149 -0
- package/dist/authorize.js.map +1 -0
- package/dist/clients.d.ts +70 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +177 -0
- package/dist/clients.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +28 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +24 -0
- package/dist/configure.js.map +1 -0
- package/dist/crypto.d.ts +35 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +56 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +32 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +110 -0
- package/dist/introspect.js.map +1 -0
- package/dist/services/main.d.ts +29 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +48 -0
- package/dist/services/main.js.map +1 -0
- package/dist/store.d.ts +44 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/stores/memory.d.ts +34 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +100 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/stubs.d.ts +14 -0
- package/dist/stubs.d.ts.map +1 -0
- package/dist/stubs.js +15 -0
- package/dist/stubs.js.map +1 -0
- package/dist/testing/main.d.ts +49 -0
- package/dist/testing/main.d.ts.map +1 -0
- package/dist/testing/main.js +77 -0
- package/dist/testing/main.js.map +1 -0
- package/dist/token.d.ts +20 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +212 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +83 -0
- package/src/VisaManager.ts +346 -0
- package/src/VisaProvider.ts +197 -0
- package/src/augmentations.ts +29 -0
- package/src/authorize.ts +250 -0
- package/src/clients.ts +231 -0
- package/src/config.ts +34 -0
- package/src/configure.ts +42 -0
- package/src/crypto.ts +59 -0
- package/src/errors.ts +79 -0
- package/src/index.ts +67 -0
- package/src/introspect.ts +139 -0
- package/src/services/main.ts +57 -0
- package/src/store.ts +56 -0
- package/src/stores/memory.ts +118 -0
- package/src/stubs.ts +16 -0
- package/src/testing/main.ts +119 -0
- package/src/token.ts +325 -0
- package/src/types.ts +134 -0
- package/stubs/config/visa.stub +25 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revocation (RFC 7009) and introspection (RFC 7662).
|
|
3
|
+
*
|
|
4
|
+
* Both answer the same way to a token that is unknown, expired, revoked or
|
|
5
|
+
* belongs to someone else — revocation with 200, introspection with
|
|
6
|
+
* `{ active: false }`. That uniformity IS the security property: a different
|
|
7
|
+
* answer per case turns either endpoint into an oracle for guessing tokens.
|
|
8
|
+
*/
|
|
9
|
+
import { authenticateClient } from "./clients.js";
|
|
10
|
+
import { hashSecret } from "./crypto.js";
|
|
11
|
+
import { OAuthError } from "./errors.js";
|
|
12
|
+
/** `{ active: false }` — the only thing an inactive token ever reveals. */
|
|
13
|
+
const INACTIVE = { active: false };
|
|
14
|
+
export async function introspect(request, credentials, store, now = new Date(), issuer) {
|
|
15
|
+
// The CALLER authenticates — introspection tells you whether a credential
|
|
16
|
+
// is live, which is exactly what an attacker wants to ask.
|
|
17
|
+
const caller = await authenticateClient(credentials, (id) => store.findClient(id));
|
|
18
|
+
const presented = asString(request.token);
|
|
19
|
+
if (presented === undefined) {
|
|
20
|
+
throw new OAuthError("invalid_request", "token is required.");
|
|
21
|
+
}
|
|
22
|
+
const hash = hashSecret(presented);
|
|
23
|
+
const hint = asString(request.token_hint) ?? asString(request.token_type_hint);
|
|
24
|
+
// The hint is an optimisation, never a filter: a wrong hint must still
|
|
25
|
+
// find the token (RFC 7662 §2.1).
|
|
26
|
+
const access = hint === "refresh_token" ? null : await store.findAccessToken(hash);
|
|
27
|
+
if (access !== null) {
|
|
28
|
+
if (access.revokedAt !== undefined || access.expiresAt <= now) {
|
|
29
|
+
return INACTIVE;
|
|
30
|
+
}
|
|
31
|
+
// A client may only introspect its own tokens.
|
|
32
|
+
if (access.clientId !== caller.id)
|
|
33
|
+
return INACTIVE;
|
|
34
|
+
return {
|
|
35
|
+
active: true,
|
|
36
|
+
...(issuer === undefined ? {} : { iss: issuer }),
|
|
37
|
+
scope: access.scopes.join(" "),
|
|
38
|
+
client_id: access.clientId,
|
|
39
|
+
token_type: "Bearer",
|
|
40
|
+
exp: Math.floor(access.expiresAt.getTime() / 1000),
|
|
41
|
+
...(access.userId === undefined ? {} : { sub: access.userId }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const refresh = await store.findRefreshToken(hash);
|
|
45
|
+
if (refresh === null)
|
|
46
|
+
return INACTIVE;
|
|
47
|
+
if (refresh.revokedAt !== undefined ||
|
|
48
|
+
refresh.consumedAt !== undefined ||
|
|
49
|
+
refresh.expiresAt <= now ||
|
|
50
|
+
refresh.clientId !== caller.id) {
|
|
51
|
+
return INACTIVE;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
active: true,
|
|
55
|
+
...(issuer === undefined ? {} : { iss: issuer }),
|
|
56
|
+
scope: refresh.scopes.join(" "),
|
|
57
|
+
client_id: refresh.clientId,
|
|
58
|
+
exp: Math.floor(refresh.expiresAt.getTime() / 1000),
|
|
59
|
+
...(refresh.userId === undefined ? {} : { sub: refresh.userId }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Revoke a token. Answers nothing, always successfully (RFC 7009 §2.2).
|
|
64
|
+
*
|
|
65
|
+
* Revoking a REFRESH token takes its whole family with it, access tokens
|
|
66
|
+
* included: "sign me out" has to mean the session is over, not that one of
|
|
67
|
+
* its two credentials was retired.
|
|
68
|
+
*/
|
|
69
|
+
export async function revoke(request, credentials, store, now = new Date()) {
|
|
70
|
+
const caller = await authenticateClient(credentials, (id) => store.findClient(id));
|
|
71
|
+
const presented = asString(request.token);
|
|
72
|
+
if (presented === undefined) {
|
|
73
|
+
throw new OAuthError("invalid_request", "token is required.");
|
|
74
|
+
}
|
|
75
|
+
const hash = hashSecret(presented);
|
|
76
|
+
const refresh = await store.findRefreshToken(hash);
|
|
77
|
+
if (refresh !== null) {
|
|
78
|
+
// Someone else's token: nothing happens, and they are not told so.
|
|
79
|
+
if (refresh.clientId !== caller.id)
|
|
80
|
+
return;
|
|
81
|
+
await store.revokeFamily(refresh.familyId, now);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const access = await store.findAccessToken(hash);
|
|
85
|
+
if (access === null || access.clientId !== caller.id)
|
|
86
|
+
return;
|
|
87
|
+
await store.revokeAccessToken(hash, now);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Is this bearer token good, and for what?
|
|
91
|
+
*
|
|
92
|
+
* What a resource server calls on every request — so it reads the store
|
|
93
|
+
* directly instead of going through introspection's client authentication.
|
|
94
|
+
*/
|
|
95
|
+
export async function verifyAccessToken(presented, store, now = new Date()) {
|
|
96
|
+
const token = await store.findAccessToken(hashSecret(presented));
|
|
97
|
+
if (token === null)
|
|
98
|
+
return null;
|
|
99
|
+
if (token.revokedAt !== undefined || token.expiresAt <= now)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
clientId: token.clientId,
|
|
103
|
+
...(token.userId === undefined ? {} : { userId: token.userId }),
|
|
104
|
+
scopes: token.scopes,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function asString(value) {
|
|
108
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=introspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspect.js","sourceRoot":"","sources":["../src/introspect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container service accessor — `import visa from '@c9up/visa/services/main'`.
|
|
3
|
+
*
|
|
4
|
+
* Populated by `VisaProvider.boot()`. Reading it before the provider booted
|
|
5
|
+
* throws rather than answering with a half-built server: an authorization
|
|
6
|
+
* server with no store would mint tokens nobody can verify.
|
|
7
|
+
*/
|
|
8
|
+
import type { VisaManager } from "../VisaManager.js";
|
|
9
|
+
/** @internal Called by the provider once the manager exists. */
|
|
10
|
+
export declare function setVisa(manager: VisaManager): void;
|
|
11
|
+
/** @internal Read the seated manager, if there is one. */
|
|
12
|
+
export declare function getVisa(): VisaManager | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* @internal Forget the manager — the provider on shutdown, tests between cases.
|
|
15
|
+
*
|
|
16
|
+
* The caller checks ownership first (`getVisa() === mine`): two applications
|
|
17
|
+
* share this module in one process, and the one shutting down must not clear a
|
|
18
|
+
* manager the other has since seated.
|
|
19
|
+
*/
|
|
20
|
+
export declare function clearVisa(): void;
|
|
21
|
+
/**
|
|
22
|
+
* A proxy so the import can be held before the provider boots.
|
|
23
|
+
*
|
|
24
|
+
* Symbols and `then` answer undefined: a module namespace is probed for `then`
|
|
25
|
+
* when it is imported, and a proxy that threw there would crash the import.
|
|
26
|
+
*/
|
|
27
|
+
declare const visa: VisaManager;
|
|
28
|
+
export default visa;
|
|
29
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAElD;AAED,0DAA0D;AAC1D,wBAAgB,OAAO,IAAI,WAAW,GAAG,SAAS,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAEhC;AAWD;;;;;GAKG;AACH,QAAA,MAAM,IAAI,EAMJ,WAAW,CAAC;AAElB,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container service accessor — `import visa from '@c9up/visa/services/main'`.
|
|
3
|
+
*
|
|
4
|
+
* Populated by `VisaProvider.boot()`. Reading it before the provider booted
|
|
5
|
+
* throws rather than answering with a half-built server: an authorization
|
|
6
|
+
* server with no store would mint tokens nobody can verify.
|
|
7
|
+
*/
|
|
8
|
+
let instance;
|
|
9
|
+
/** @internal Called by the provider once the manager exists. */
|
|
10
|
+
export function setVisa(manager) {
|
|
11
|
+
instance = manager;
|
|
12
|
+
}
|
|
13
|
+
/** @internal Read the seated manager, if there is one. */
|
|
14
|
+
export function getVisa() {
|
|
15
|
+
return instance;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @internal Forget the manager — the provider on shutdown, tests between cases.
|
|
19
|
+
*
|
|
20
|
+
* The caller checks ownership first (`getVisa() === mine`): two applications
|
|
21
|
+
* share this module in one process, and the one shutting down must not clear a
|
|
22
|
+
* manager the other has since seated.
|
|
23
|
+
*/
|
|
24
|
+
export function clearVisa() {
|
|
25
|
+
instance = undefined;
|
|
26
|
+
}
|
|
27
|
+
function resolve() {
|
|
28
|
+
if (!instance) {
|
|
29
|
+
throw new Error("[visa] accessed before initialization — register VisaProvider, or call setVisa() yourself.");
|
|
30
|
+
}
|
|
31
|
+
return instance;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A proxy so the import can be held before the provider boots.
|
|
35
|
+
*
|
|
36
|
+
* Symbols and `then` answer undefined: a module namespace is probed for `then`
|
|
37
|
+
* when it is imported, and a proxy that threw there would crash the import.
|
|
38
|
+
*/
|
|
39
|
+
const visa = new Proxy({}, {
|
|
40
|
+
get(_target, property) {
|
|
41
|
+
if (typeof property === "symbol" || property === "then")
|
|
42
|
+
return undefined;
|
|
43
|
+
const value = Reflect.get(resolve(), property);
|
|
44
|
+
return typeof value === "function" ? value.bind(resolve()) : value;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
export default visa;
|
|
48
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,IAAI,QAAiC,CAAC;AAEtC,gEAAgE;AAChE,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC3C,QAAQ,GAAG,OAAO,CAAC;AACpB,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,OAAO;IACtB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS;IACxB,QAAQ,GAAG,SAAS,CAAC;AACtB,CAAC;AAED,SAAS,OAAO;IACf,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACd,4FAA4F,CAC5F,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,EAAiB,EAAE;IACzC,GAAG,CAAC,OAAO,EAAE,QAAQ;QACpB,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,MAAM;YAAE,OAAO,SAAS,CAAC;QAC1E,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC/C,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACpE,CAAC;CACD,CAAgB,CAAC;AAElB,eAAe,IAAI,CAAC"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the server needs from wherever its state lives.
|
|
3
|
+
*
|
|
4
|
+
* A contract rather than a table, the same way echo, eclipse and bay do it:
|
|
5
|
+
* an application already running atlas gets a driver, one running on Redis or
|
|
6
|
+
* on a schema it does not own writes twelve methods.
|
|
7
|
+
*
|
|
8
|
+
* Every method that takes a credential takes it HASHED. The plaintext never
|
|
9
|
+
* reaches the store, so a database dump is not a set of working tokens.
|
|
10
|
+
*/
|
|
11
|
+
import type { AccessToken, AuthorizationCode, Client, Consent, RefreshToken } from "./types.js";
|
|
12
|
+
export interface VisaStore {
|
|
13
|
+
findClient(id: string): Promise<Client | null>;
|
|
14
|
+
saveAuthorizationCode(code: AuthorizationCode): Promise<void>;
|
|
15
|
+
findAuthorizationCode(codeHash: string): Promise<AuthorizationCode | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Mark a code as used. MUST be atomic against a concurrent exchange: two
|
|
18
|
+
* requests racing with the same code must not both succeed, which is the
|
|
19
|
+
* whole of the single-use guarantee (§7.5.1).
|
|
20
|
+
*
|
|
21
|
+
* Answers `false` when it was already consumed.
|
|
22
|
+
*/
|
|
23
|
+
consumeAuthorizationCode(codeHash: string, at: Date): Promise<boolean>;
|
|
24
|
+
saveAccessToken(token: AccessToken): Promise<void>;
|
|
25
|
+
findAccessToken(tokenHash: string): Promise<AccessToken | null>;
|
|
26
|
+
revokeAccessToken(tokenHash: string, at: Date): Promise<void>;
|
|
27
|
+
saveRefreshToken(token: RefreshToken): Promise<void>;
|
|
28
|
+
findRefreshToken(tokenHash: string): Promise<RefreshToken | null>;
|
|
29
|
+
/** Same atomicity requirement as a code: rotation depends on it. */
|
|
30
|
+
consumeRefreshToken(tokenHash: string, at: Date): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Revoke every token of a family, refresh and access alike.
|
|
33
|
+
*
|
|
34
|
+
* Called when a consumed refresh token comes back: that is a leak, and the
|
|
35
|
+
* only safe answer is to end the session it belongs to rather than guess
|
|
36
|
+
* which of the two holders is the thief.
|
|
37
|
+
*/
|
|
38
|
+
revokeFamily(familyId: string, at: Date): Promise<void>;
|
|
39
|
+
findConsent(userId: string, clientId: string): Promise<Consent | null>;
|
|
40
|
+
saveConsent(consent: Consent): Promise<void>;
|
|
41
|
+
/** Drop what has expired. Optional: a store with a TTL does it itself. */
|
|
42
|
+
prune?(now: Date): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACX,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,OAAO,EACP,YAAY,EACZ,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,SAAS;IACzB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE/C,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC3E;;;;;;OAMG;IACH,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvE,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAChE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAClE,oEAAoE;IACpE,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE;;;;;;OAMG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACvE,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C,0EAA0E;IAC1E,KAAK,CAAC,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the server needs from wherever its state lives.
|
|
3
|
+
*
|
|
4
|
+
* A contract rather than a table, the same way echo, eclipse and bay do it:
|
|
5
|
+
* an application already running atlas gets a driver, one running on Redis or
|
|
6
|
+
* on a schema it does not own writes twelve methods.
|
|
7
|
+
*
|
|
8
|
+
* Every method that takes a credential takes it HASHED. The plaintext never
|
|
9
|
+
* reaches the store, so a database dump is not a set of working tokens.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store that needs no database.
|
|
3
|
+
*
|
|
4
|
+
* For tests and for a single-process development server — never for
|
|
5
|
+
* production, where a restart would sign every user out and a second instance
|
|
6
|
+
* would not see the first one's tokens.
|
|
7
|
+
*
|
|
8
|
+
* It is also the reference implementation of the contract: the atomicity the
|
|
9
|
+
* interface demands is trivially true here, in one event loop, which is
|
|
10
|
+
* exactly why a driver on a real database has to say how it achieves it.
|
|
11
|
+
*/
|
|
12
|
+
import type { VisaStore } from "../store.js";
|
|
13
|
+
import type { AccessToken, AuthorizationCode, Client, Consent, RefreshToken } from "../types.js";
|
|
14
|
+
export declare class MemoryStore implements VisaStore {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(clients?: readonly Client[]);
|
|
17
|
+
/** Register a client. Returns it, so a caller can keep the reference. */
|
|
18
|
+
addClient(client: Client): Client;
|
|
19
|
+
findClient(id: string): Promise<Client | null>;
|
|
20
|
+
saveAuthorizationCode(code: AuthorizationCode): Promise<void>;
|
|
21
|
+
findAuthorizationCode(codeHash: string): Promise<AuthorizationCode | null>;
|
|
22
|
+
consumeAuthorizationCode(codeHash: string, at: Date): Promise<boolean>;
|
|
23
|
+
saveAccessToken(token: AccessToken): Promise<void>;
|
|
24
|
+
findAccessToken(tokenHash: string): Promise<AccessToken | null>;
|
|
25
|
+
revokeAccessToken(tokenHash: string, at: Date): Promise<void>;
|
|
26
|
+
saveRefreshToken(token: RefreshToken): Promise<void>;
|
|
27
|
+
findRefreshToken(tokenHash: string): Promise<RefreshToken | null>;
|
|
28
|
+
consumeRefreshToken(tokenHash: string, at: Date): Promise<boolean>;
|
|
29
|
+
revokeFamily(familyId: string, at: Date): Promise<void>;
|
|
30
|
+
findConsent(userId: string, clientId: string): Promise<Consent | null>;
|
|
31
|
+
saveConsent(consent: Consent): Promise<void>;
|
|
32
|
+
prune(now: Date): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EACX,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,OAAO,EACP,YAAY,EACZ,MAAM,aAAa,CAAC;AAErB,qBAAa,WAAY,YAAW,SAAS;;gBAOhC,OAAO,GAAE,SAAS,MAAM,EAAO;IAI3C,yEAAyE;IACzE,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAK3B,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAI9C,qBAAqB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D,qBAAqB,CAC1B,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAI9B,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAOtE,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAI/D,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAIjE,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAOlE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAItE,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C,KAAK,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAWrC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store that needs no database.
|
|
3
|
+
*
|
|
4
|
+
* For tests and for a single-process development server — never for
|
|
5
|
+
* production, where a restart would sign every user out and a second instance
|
|
6
|
+
* would not see the first one's tokens.
|
|
7
|
+
*
|
|
8
|
+
* It is also the reference implementation of the contract: the atomicity the
|
|
9
|
+
* interface demands is trivially true here, in one event loop, which is
|
|
10
|
+
* exactly why a driver on a real database has to say how it achieves it.
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryStore {
|
|
13
|
+
#clients = new Map();
|
|
14
|
+
#codes = new Map();
|
|
15
|
+
#accessTokens = new Map();
|
|
16
|
+
#refreshTokens = new Map();
|
|
17
|
+
#consents = new Map();
|
|
18
|
+
constructor(clients = []) {
|
|
19
|
+
for (const client of clients)
|
|
20
|
+
this.#clients.set(client.id, client);
|
|
21
|
+
}
|
|
22
|
+
/** Register a client. Returns it, so a caller can keep the reference. */
|
|
23
|
+
addClient(client) {
|
|
24
|
+
this.#clients.set(client.id, client);
|
|
25
|
+
return client;
|
|
26
|
+
}
|
|
27
|
+
async findClient(id) {
|
|
28
|
+
return this.#clients.get(id) ?? null;
|
|
29
|
+
}
|
|
30
|
+
async saveAuthorizationCode(code) {
|
|
31
|
+
this.#codes.set(code.codeHash, code);
|
|
32
|
+
}
|
|
33
|
+
async findAuthorizationCode(codeHash) {
|
|
34
|
+
return this.#codes.get(codeHash) ?? null;
|
|
35
|
+
}
|
|
36
|
+
async consumeAuthorizationCode(codeHash, at) {
|
|
37
|
+
const code = this.#codes.get(codeHash);
|
|
38
|
+
if (code === undefined || code.consumedAt !== undefined)
|
|
39
|
+
return false;
|
|
40
|
+
code.consumedAt = at;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
async saveAccessToken(token) {
|
|
44
|
+
this.#accessTokens.set(token.tokenHash, token);
|
|
45
|
+
}
|
|
46
|
+
async findAccessToken(tokenHash) {
|
|
47
|
+
return this.#accessTokens.get(tokenHash) ?? null;
|
|
48
|
+
}
|
|
49
|
+
async revokeAccessToken(tokenHash, at) {
|
|
50
|
+
const token = this.#accessTokens.get(tokenHash);
|
|
51
|
+
if (token !== undefined)
|
|
52
|
+
token.revokedAt = at;
|
|
53
|
+
}
|
|
54
|
+
async saveRefreshToken(token) {
|
|
55
|
+
this.#refreshTokens.set(token.tokenHash, token);
|
|
56
|
+
}
|
|
57
|
+
async findRefreshToken(tokenHash) {
|
|
58
|
+
return this.#refreshTokens.get(tokenHash) ?? null;
|
|
59
|
+
}
|
|
60
|
+
async consumeRefreshToken(tokenHash, at) {
|
|
61
|
+
const token = this.#refreshTokens.get(tokenHash);
|
|
62
|
+
if (token === undefined || token.consumedAt !== undefined)
|
|
63
|
+
return false;
|
|
64
|
+
token.consumedAt = at;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
async revokeFamily(familyId, at) {
|
|
68
|
+
// Both kinds: a leaked refresh token has usually already bought an
|
|
69
|
+
// access token, and leaving that one alive defeats the revocation.
|
|
70
|
+
for (const token of this.#refreshTokens.values()) {
|
|
71
|
+
if (token.familyId === familyId)
|
|
72
|
+
token.revokedAt = at;
|
|
73
|
+
}
|
|
74
|
+
for (const token of this.#accessTokens.values()) {
|
|
75
|
+
if (token.familyId === familyId)
|
|
76
|
+
token.revokedAt = at;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async findConsent(userId, clientId) {
|
|
80
|
+
return this.#consents.get(`${userId}\u0000${clientId}`) ?? null;
|
|
81
|
+
}
|
|
82
|
+
async saveConsent(consent) {
|
|
83
|
+
this.#consents.set(`${consent.userId}\u0000${consent.clientId}`, consent);
|
|
84
|
+
}
|
|
85
|
+
async prune(now) {
|
|
86
|
+
for (const [key, code] of this.#codes) {
|
|
87
|
+
if (code.expiresAt <= now)
|
|
88
|
+
this.#codes.delete(key);
|
|
89
|
+
}
|
|
90
|
+
for (const [key, token] of this.#accessTokens) {
|
|
91
|
+
if (token.expiresAt <= now)
|
|
92
|
+
this.#accessTokens.delete(key);
|
|
93
|
+
}
|
|
94
|
+
for (const [key, token] of this.#refreshTokens) {
|
|
95
|
+
if (token.expiresAt <= now)
|
|
96
|
+
this.#refreshTokens.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"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"}
|
package/dist/stubs.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this package's stubs live.
|
|
3
|
+
*
|
|
4
|
+
* Its own module, as upstream keeps it: a stub root is a path a package
|
|
5
|
+
* publishes, and a caller importing it should not have to import the
|
|
6
|
+
* configure hook to get it.
|
|
7
|
+
*
|
|
8
|
+
* `import.meta.dirname` rather than a URL — upstream reads it straight off
|
|
9
|
+
* `import.meta`, and deriving it from `import.meta.url` breaks wherever that
|
|
10
|
+
* is not a `file://` URL, which is what a test runner serving modules over
|
|
11
|
+
* HTTP hands you.
|
|
12
|
+
*/
|
|
13
|
+
export declare const stubsRoot: string;
|
|
14
|
+
//# sourceMappingURL=stubs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stubs.d.ts","sourceRoot":"","sources":["../src/stubs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,eAAO,MAAM,SAAS,QAA2C,CAAC"}
|
package/dist/stubs.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this package's stubs live.
|
|
3
|
+
*
|
|
4
|
+
* Its own module, as upstream keeps it: a stub root is a path a package
|
|
5
|
+
* publishes, and a caller importing it should not have to import the
|
|
6
|
+
* configure hook to get it.
|
|
7
|
+
*
|
|
8
|
+
* `import.meta.dirname` rather than a URL — upstream reads it straight off
|
|
9
|
+
* `import.meta`, and deriving it from `import.meta.url` breaks wherever that
|
|
10
|
+
* is not a `file://` URL, which is what a test runner serving modules over
|
|
11
|
+
* HTTP hands you.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
export const stubsRoot = join(import.meta.dirname, "..", "stubs");
|
|
15
|
+
//# sourceMappingURL=stubs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stubs.js","sourceRoot":"","sources":["../src/stubs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers — `import { testVisa } from '@c9up/visa/testing'`.
|
|
3
|
+
*
|
|
4
|
+
* Not a fake of the server: the real one, on a memory store. An authorization
|
|
5
|
+
* server whose test double is lenient teaches applications to ship the leniency
|
|
6
|
+
* — a consent screen tested against a stub that always grants is a consent
|
|
7
|
+
* screen nobody has ever seen refuse.
|
|
8
|
+
*
|
|
9
|
+
* What it removes is the setup, not the rules: a registered client, a PKCE
|
|
10
|
+
* pair, and one call that walks authorize → consent → code → token.
|
|
11
|
+
*/
|
|
12
|
+
import { MemoryStore } from "../stores/memory.js";
|
|
13
|
+
import type { Client, GrantType, TokenResponse } from "../types.js";
|
|
14
|
+
import { type VisaConfig, VisaManager } from "../VisaManager.js";
|
|
15
|
+
export interface TestVisa {
|
|
16
|
+
visa: VisaManager;
|
|
17
|
+
store: MemoryStore;
|
|
18
|
+
/** The client that was registered for you. */
|
|
19
|
+
client: Client;
|
|
20
|
+
/** Its secret, in the clear — it exists only in this process. */
|
|
21
|
+
secret: string;
|
|
22
|
+
/** Credentials shaped the way the endpoints take them. */
|
|
23
|
+
credentials: {
|
|
24
|
+
clientId: string;
|
|
25
|
+
clientSecret?: string;
|
|
26
|
+
};
|
|
27
|
+
/** A fresh PKCE pair. */
|
|
28
|
+
pkce(): {
|
|
29
|
+
verifier: string;
|
|
30
|
+
challenge: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Everything at once: authorize as `userId`, consent, exchange the code.
|
|
34
|
+
* What a test protecting a resource needs, without the four steps.
|
|
35
|
+
*/
|
|
36
|
+
tokensFor(userId: string, scope?: string): Promise<TokenResponse>;
|
|
37
|
+
}
|
|
38
|
+
export interface TestVisaOptions {
|
|
39
|
+
issuer?: string;
|
|
40
|
+
clientId?: string;
|
|
41
|
+
redirectUri?: string;
|
|
42
|
+
scopes?: string[];
|
|
43
|
+
grantTypes?: GrantType[];
|
|
44
|
+
/** A public client: no secret, and no client_credentials. */
|
|
45
|
+
publicClient?: boolean;
|
|
46
|
+
config?: Partial<Omit<VisaConfig, "issuer" | "store">>;
|
|
47
|
+
}
|
|
48
|
+
export declare function testVisa(options?: TestVisaOptions): Promise<TestVisa>;
|
|
49
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/testing/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEjE,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,WAAW,CAAC;IACnB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,yBAAyB;IACzB,IAAI,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,wBAAsB,QAAQ,CAC7B,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAsEnB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers — `import { testVisa } from '@c9up/visa/testing'`.
|
|
3
|
+
*
|
|
4
|
+
* Not a fake of the server: the real one, on a memory store. An authorization
|
|
5
|
+
* server whose test double is lenient teaches applications to ship the leniency
|
|
6
|
+
* — a consent screen tested against a stub that always grants is a consent
|
|
7
|
+
* screen nobody has ever seen refuse.
|
|
8
|
+
*
|
|
9
|
+
* What it removes is the setup, not the rules: a registered client, a PKCE
|
|
10
|
+
* pair, and one call that walks authorize → consent → code → token.
|
|
11
|
+
*/
|
|
12
|
+
import { randomToken, sha256 } from "../crypto.js";
|
|
13
|
+
import { MemoryStore } from "../stores/memory.js";
|
|
14
|
+
import { VisaManager } from "../VisaManager.js";
|
|
15
|
+
export async function testVisa(options = {}) {
|
|
16
|
+
const redirectUri = options.redirectUri ?? "https://app.test/callback";
|
|
17
|
+
const store = new MemoryStore();
|
|
18
|
+
const visa = new VisaManager({
|
|
19
|
+
issuer: options.issuer ?? "https://auth.test",
|
|
20
|
+
store,
|
|
21
|
+
...options.config,
|
|
22
|
+
});
|
|
23
|
+
const { client, secret } = await visa.registerClient({
|
|
24
|
+
id: options.clientId ?? "test-client",
|
|
25
|
+
name: "Test client",
|
|
26
|
+
redirectUris: [redirectUri],
|
|
27
|
+
scopes: options.scopes ?? ["profile", "email"],
|
|
28
|
+
grantTypes: options.grantTypes ?? ["authorization_code", "refresh_token"],
|
|
29
|
+
...(options.publicClient === true
|
|
30
|
+
? { tokenEndpointAuthMethod: "none" }
|
|
31
|
+
: {}),
|
|
32
|
+
});
|
|
33
|
+
const credentials = secret === undefined
|
|
34
|
+
? { clientId: client.id }
|
|
35
|
+
: { clientId: client.id, clientSecret: secret };
|
|
36
|
+
return {
|
|
37
|
+
visa,
|
|
38
|
+
store,
|
|
39
|
+
client,
|
|
40
|
+
secret: secret ?? "",
|
|
41
|
+
credentials,
|
|
42
|
+
pkce() {
|
|
43
|
+
const verifier = randomToken();
|
|
44
|
+
return { verifier, challenge: sha256(verifier) };
|
|
45
|
+
},
|
|
46
|
+
async tokensFor(userId, scope) {
|
|
47
|
+
const verifier = randomToken();
|
|
48
|
+
const outcome = await visa.authorize({
|
|
49
|
+
response_type: "code",
|
|
50
|
+
client_id: client.id,
|
|
51
|
+
redirect_uri: redirectUri,
|
|
52
|
+
code_challenge: sha256(verifier),
|
|
53
|
+
code_challenge_method: "S256",
|
|
54
|
+
...(scope === undefined ? {} : { scope }),
|
|
55
|
+
}, userId);
|
|
56
|
+
// The real path, consent screen included — a helper that skipped it
|
|
57
|
+
// would let an application forget the screen and still pass.
|
|
58
|
+
const url = outcome.type === "consent"
|
|
59
|
+
? await visa.grant(outcome.request, userId)
|
|
60
|
+
: outcome.type === "redirect"
|
|
61
|
+
? outcome.url
|
|
62
|
+
: (() => {
|
|
63
|
+
throw new Error(`authorize failed: ${outcome.error.message}`);
|
|
64
|
+
})();
|
|
65
|
+
const code = new URL(url).searchParams.get("code");
|
|
66
|
+
if (code === null)
|
|
67
|
+
throw new Error(`no code in ${url}`);
|
|
68
|
+
return visa.token({
|
|
69
|
+
grant_type: "authorization_code",
|
|
70
|
+
code,
|
|
71
|
+
redirect_uri: redirectUri,
|
|
72
|
+
code_verifier: verifier,
|
|
73
|
+
}, credentials);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/testing/main.ts"],"names":[],"mappings":"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"}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token endpoint — the three grants OAuth 2.1 keeps.
|
|
3
|
+
*
|
|
4
|
+
* `password` and `implicit` are gone from the spec and are not implemented
|
|
5
|
+
* here under any option: both hand a credential to a party that should never
|
|
6
|
+
* hold one, and an application that needs them needs a different design.
|
|
7
|
+
*/
|
|
8
|
+
import { type ClientCredentials } from "./clients.js";
|
|
9
|
+
import type { VisaStore } from "./store.js";
|
|
10
|
+
import type { TokenResponse } from "./types.js";
|
|
11
|
+
export interface TokenOptions {
|
|
12
|
+
accessTokenTtlSeconds?: number;
|
|
13
|
+
refreshTokenTtlSeconds?: number;
|
|
14
|
+
/** Hand out refresh tokens at all. */
|
|
15
|
+
issueRefreshTokens?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The body of a token request, untrusted. */
|
|
18
|
+
export type TokenRequest = Record<string, unknown>;
|
|
19
|
+
export declare function token(request: TokenRequest, credentials: ClientCredentials, store: VisaStore, options?: TokenOptions, now?: Date): Promise<TokenResponse>;
|
|
20
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAGN,KAAK,iBAAiB,EAEtB,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAqB,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,YAAY;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAQD,8CAA8C;AAC9C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnD,wBAAsB,KAAK,CAC1B,OAAO,EAAE,YAAY,EACrB,WAAW,EAAE,iBAAiB,EAC9B,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,YAAiB,EAC1B,GAAG,GAAE,IAAiB,GACpB,OAAO,CAAC,aAAa,CAAC,CA6BxB"}
|