@soulcode/bexio-oauth 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/claims.d.ts +3 -3
- package/dist/claims.d.ts.map +1 -1
- package/dist/claims.js +5 -5
- package/dist/claims.js.map +1 -1
- package/dist/client.d.ts +4 -137
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -629
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -0
- package/dist/connect-flow.d.ts +15 -0
- package/dist/connect-flow.d.ts.map +1 -0
- package/dist/connect-flow.js +123 -0
- package/dist/connect-flow.js.map +1 -0
- package/dist/crypto.d.ts +2 -2
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +20 -15
- package/dist/crypto.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/external-tokens.d.ts +8 -0
- package/dist/external-tokens.d.ts.map +1 -0
- package/dist/external-tokens.js +73 -0
- package/dist/external-tokens.js.map +1 -0
- package/dist/flow-cookie.d.ts +23 -0
- package/dist/flow-cookie.d.ts.map +1 -0
- package/dist/flow-cookie.js +44 -0
- package/dist/flow-cookie.js.map +1 -0
- package/dist/grants.d.ts +45 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +143 -0
- package/dist/grants.js.map +1 -0
- package/dist/idle-sweep.d.ts +11 -0
- package/dist/idle-sweep.d.ts.map +1 -0
- package/dist/idle-sweep.js +70 -0
- package/dist/idle-sweep.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations.generated.d.ts +3 -0
- package/dist/migrations.generated.d.ts.map +1 -0
- package/dist/migrations.generated.js +56 -0
- package/dist/migrations.generated.js.map +1 -0
- package/dist/oidc.d.ts +30 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +67 -0
- package/dist/oidc.js.map +1 -0
- package/dist/refresh.d.ts +30 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +147 -0
- package/dist/refresh.js.map +1 -0
- package/dist/return-to.d.ts +8 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +31 -0
- package/dist/return-to.js.map +1 -0
- package/dist/sql.d.ts +6 -1
- package/dist/sql.d.ts.map +1 -1
- package/dist/sql.js +8 -12
- package/dist/sql.js.map +1 -1
- package/dist/store/memory.d.ts.map +1 -1
- package/dist/store/memory.js +82 -46
- package/dist/store/memory.js.map +1 -1
- package/dist/store/postgres.d.ts +4 -4
- package/dist/store/postgres.d.ts.map +1 -1
- package/dist/store/postgres.js +137 -59
- package/dist/store/postgres.js.map +1 -1
- package/dist/store/types.d.ts +61 -21
- package/dist/store/types.d.ts.map +1 -1
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +3 -1
- package/sql/0001_bexio_oauth.sql +48 -0
- package/sql/0001_bexio_grants.sql +0 -32
package/dist/grants.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ClaimBag } from "./claims.js";
|
|
2
|
+
import type { ClientContext } from "./config.js";
|
|
3
|
+
import type { TokenCipher } from "./crypto.js";
|
|
4
|
+
import type { ConnectionRecord, GrantRecord, GrantStore } from "./store/types.js";
|
|
5
|
+
import type { AccessToken, Grant, GrantSelector } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* The app-facing view of a stored grant, with `c` (default: the grant's own)
|
|
8
|
+
* as its connection: no token material.
|
|
9
|
+
*/
|
|
10
|
+
export declare function toGrant(grant: GrantRecord, c?: ConnectionRecord): Grant;
|
|
11
|
+
/** The binding a connection's ciphertexts are encrypted under. */
|
|
12
|
+
export declare function bindingOf(connection: ConnectionRecord): string;
|
|
13
|
+
export declare function decryptFor(cipher: TokenCipher, connection: ConnectionRecord, value: string): Promise<string>;
|
|
14
|
+
/** Decrypts the connection's access token for the owner of `grant`. */
|
|
15
|
+
export declare function issue(cipher: TokenCipher, grant: GrantRecord, connection: ConnectionRecord): Promise<AccessToken>;
|
|
16
|
+
/** Exactly one grant of the owner, or `not_connected` / `ambiguous_grant`. */
|
|
17
|
+
export declare function resolveGrant(store: GrantStore, selector: GrantSelector): Promise<GrantRecord>;
|
|
18
|
+
/** The token's claims if it is a JWT, else none (Bexio access tokens usually are). */
|
|
19
|
+
export declare function decodeIfJwt(token: string): ClaimBag;
|
|
20
|
+
/**
|
|
21
|
+
* Adds userinfo claims when the tokens lack the profile claims. Bexio's access
|
|
22
|
+
* token carries `company_id` but no company name, email or person name; the
|
|
23
|
+
* ID token has them, so the extra call only happens without one (`saveGrant`
|
|
24
|
+
* with an access token alone). Token claims win over userinfo; a userinfo
|
|
25
|
+
* failure keeps the token claims.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withUserInfo(ctx: ClientContext, claims: ClaimBag, accessToken: string): Promise<ClaimBag>;
|
|
28
|
+
/**
|
|
29
|
+
* Encrypts the tokens, reads the role, and upserts the connection for the
|
|
30
|
+
* identity in `claims` and the owner's grant on it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function saveTokens(ctx: ClientContext, input: {
|
|
33
|
+
ownerId: string;
|
|
34
|
+
claims: ClaimBag;
|
|
35
|
+
accessToken: string;
|
|
36
|
+
refreshToken: string | null;
|
|
37
|
+
accessExpiresAt: Date | null;
|
|
38
|
+
refreshExpiresAt: Date | null;
|
|
39
|
+
scope: string | null;
|
|
40
|
+
}): Promise<GrantRecord>;
|
|
41
|
+
export declare function listGrants(store: GrantStore, query: {
|
|
42
|
+
ownerId: string;
|
|
43
|
+
}): Promise<Grant[]>;
|
|
44
|
+
export declare function disconnect(ctx: ClientContext, selector: GrantSelector): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgC,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE;;;GAGG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,WAAW,EAClB,CAAC,GAAE,gBAAmC,GACrC,KAAK,CAmBP;AAED,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,UAAU,EAAE,gBAAgB,GAAG,MAAM,CAE9D;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,gBAAgB,EAC5B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,uEAAuE;AACvE,wBAAsB,KAAK,CACzB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,gBAAgB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,8EAA8E;AAC9E,wBAAsB,YAAY,CAChC,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,WAAW,CAAC,CAatB;AAED,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAMnD;AAKD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,QAAQ,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAanB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE;IACL,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAC7B,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GACA,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,CAAC,KAAK,EAAE,CAAC,CAMlB;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
package/dist/grants.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { decodeJwt } from "jose";
|
|
3
|
+
import { connectionBinding, mapClaims } from "./claims.js";
|
|
4
|
+
import { BexioOAuthError } from "./errors.js";
|
|
5
|
+
import { fetchRole } from "./permissions.js";
|
|
6
|
+
/**
|
|
7
|
+
* The app-facing view of a stored grant, with `c` (default: the grant's own)
|
|
8
|
+
* as its connection: no token material.
|
|
9
|
+
*/
|
|
10
|
+
export function toGrant(grant, c = grant.connection) {
|
|
11
|
+
return {
|
|
12
|
+
id: grant.id,
|
|
13
|
+
ownerId: grant.ownerId,
|
|
14
|
+
connectionId: c.id,
|
|
15
|
+
companyId: c.companyId,
|
|
16
|
+
companyName: c.companyName,
|
|
17
|
+
userId: c.userId,
|
|
18
|
+
userName: c.userName,
|
|
19
|
+
userEmail: c.userEmail,
|
|
20
|
+
companyUserId: c.companyUserId,
|
|
21
|
+
role: c.role,
|
|
22
|
+
status: c.status,
|
|
23
|
+
scope: c.scope,
|
|
24
|
+
accessExpiresAt: c.accessExpiresAt,
|
|
25
|
+
lastRefreshedAt: c.lastRefreshedAt,
|
|
26
|
+
createdAt: grant.createdAt,
|
|
27
|
+
updatedAt: c.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The binding a connection's ciphertexts are encrypted under. */
|
|
31
|
+
export function bindingOf(connection) {
|
|
32
|
+
return connectionBinding(connection.companyId, connection.userId);
|
|
33
|
+
}
|
|
34
|
+
export function decryptFor(cipher, connection, value) {
|
|
35
|
+
return cipher.decrypt(value, bindingOf(connection));
|
|
36
|
+
}
|
|
37
|
+
/** Decrypts the connection's access token for the owner of `grant`. */
|
|
38
|
+
export async function issue(cipher, grant, connection) {
|
|
39
|
+
return {
|
|
40
|
+
accessToken: await decryptFor(cipher, connection, connection.accessToken),
|
|
41
|
+
expiresAt: connection.accessExpiresAt,
|
|
42
|
+
grant: toGrant(grant, connection),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Exactly one grant of the owner, or `not_connected` / `ambiguous_grant`. */
|
|
46
|
+
export async function resolveGrant(store, selector) {
|
|
47
|
+
if (!selector?.ownerId)
|
|
48
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
49
|
+
const rows = await store.findGrants(selector);
|
|
50
|
+
if (rows.length === 0)
|
|
51
|
+
throw new BexioOAuthError("not_connected", "owner has no matching grant");
|
|
52
|
+
if (rows.length > 1) {
|
|
53
|
+
throw new BexioOAuthError("ambiguous_grant", "owner has several grants; pass companyId or grantId");
|
|
54
|
+
}
|
|
55
|
+
return rows[0];
|
|
56
|
+
}
|
|
57
|
+
/** The token's claims if it is a JWT, else none (Bexio access tokens usually are). */
|
|
58
|
+
export function decodeIfJwt(token) {
|
|
59
|
+
try {
|
|
60
|
+
return decodeJwt(token);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Claims a connection needs beyond the ids (see `mapClaims`). */
|
|
67
|
+
const PROFILE_CLAIMS = ["company_id", "company_name", "email"];
|
|
68
|
+
/**
|
|
69
|
+
* Adds userinfo claims when the tokens lack the profile claims. Bexio's access
|
|
70
|
+
* token carries `company_id` but no company name, email or person name; the
|
|
71
|
+
* ID token has them, so the extra call only happens without one (`saveGrant`
|
|
72
|
+
* with an access token alone). Token claims win over userinfo; a userinfo
|
|
73
|
+
* failure keeps the token claims.
|
|
74
|
+
*/
|
|
75
|
+
export async function withUserInfo(ctx, claims, accessToken) {
|
|
76
|
+
if (PROFILE_CLAIMS.every((claim) => claims[claim] !== undefined))
|
|
77
|
+
return claims;
|
|
78
|
+
try {
|
|
79
|
+
const info = await oidc.fetchUserInfo(await ctx.oidc.config(), accessToken, typeof claims.sub === "string" ? claims.sub : oidc.skipSubjectCheck);
|
|
80
|
+
return { ...info, ...claims };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return claims;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Encrypts the tokens, reads the role, and upserts the connection for the
|
|
88
|
+
* identity in `claims` and the owner's grant on it.
|
|
89
|
+
*/
|
|
90
|
+
export async function saveTokens(ctx, input) {
|
|
91
|
+
const identity = mapClaims(input.claims);
|
|
92
|
+
const binding = connectionBinding(identity.companyId, identity.userId);
|
|
93
|
+
const role = (await fetchRole(ctx.fetchImpl, ctx.apiBaseUrl, input.accessToken)) ??
|
|
94
|
+
"member";
|
|
95
|
+
const issuedAt = decodeIfJwt(input.accessToken).iat;
|
|
96
|
+
return ctx.store.upsertGrant({
|
|
97
|
+
ownerId: input.ownerId,
|
|
98
|
+
...identity,
|
|
99
|
+
role,
|
|
100
|
+
accessToken: await ctx.cipher.encrypt(input.accessToken, binding),
|
|
101
|
+
refreshToken: input.refreshToken
|
|
102
|
+
? await ctx.cipher.encrypt(input.refreshToken, binding)
|
|
103
|
+
: null,
|
|
104
|
+
accessExpiresAt: input.accessExpiresAt,
|
|
105
|
+
refreshExpiresAt: input.refreshExpiresAt,
|
|
106
|
+
scope: input.scope,
|
|
107
|
+
authorizedAt: typeof issuedAt === "number" ? new Date(issuedAt * 1000) : ctx.clock(),
|
|
108
|
+
now: ctx.clock(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export async function listGrants(store, query) {
|
|
112
|
+
if (!query?.ownerId)
|
|
113
|
+
throw new BexioOAuthError("invalid_config", "ownerId is required");
|
|
114
|
+
return (await store.findGrants({ ownerId: query.ownerId })).map((g) => toGrant(g));
|
|
115
|
+
}
|
|
116
|
+
export async function disconnect(ctx, selector) {
|
|
117
|
+
const grant = await resolveGrant(ctx.store, selector);
|
|
118
|
+
// Delete first, revoke after: revoking ends the one refresh token chain
|
|
119
|
+
// all owners of this identity share, so it may only happen once the store
|
|
120
|
+
// has confirmed that no grant (not even a concurrent one) uses it.
|
|
121
|
+
await ctx.store.deleteGrant(grant.id);
|
|
122
|
+
const deleted = await ctx.store.deleteUnusedConnection(grant.connection.id);
|
|
123
|
+
if (!deleted)
|
|
124
|
+
return;
|
|
125
|
+
const { connection, userHasOtherConnections } = deleted;
|
|
126
|
+
// Revoking ends all of the user's chains at Bexio, whichever token it
|
|
127
|
+
// names: a stale (reauth_required) token would still end the chain the user
|
|
128
|
+
// authorized since, and any token the user's other company's connection.
|
|
129
|
+
if (!connection.refreshToken ||
|
|
130
|
+
connection.status !== "active" ||
|
|
131
|
+
userHasOtherConnections)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const c = await ctx.oidc.config();
|
|
135
|
+
if (c.serverMetadata().revocation_endpoint) {
|
|
136
|
+
await oidc.tokenRevocation(c, await decryptFor(ctx.cipher, connection, connection.refreshToken), { token_type_hint: "refresh_token" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Revocation is best effort; the connection is deleted either way.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClientContext } from "./config.js";
|
|
2
|
+
import { type Duration } from "./duration.js";
|
|
3
|
+
import type { RefreshIdleResult } from "./types.js";
|
|
4
|
+
export declare const DEFAULT_IDLE_LIMIT = 100;
|
|
5
|
+
/** `BexioOAuth.refreshIdle`: refreshes idle connections one after another. */
|
|
6
|
+
export declare function refreshIdle(ctx: ClientContext, options: {
|
|
7
|
+
idleFor: Duration;
|
|
8
|
+
limit?: number;
|
|
9
|
+
timeBudget?: Duration;
|
|
10
|
+
}): Promise<RefreshIdleResult>;
|
|
11
|
+
//# sourceMappingURL=idle-sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-sweep.d.ts","sourceRoot":"","sources":["../src/idle-sweep.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,8EAA8E;AAC9E,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,QAAQ,CAAA;CAAE,GACpE,OAAO,CAAC,iBAAiB,CAAC,CA2D5B"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { toMs } from "./duration.js";
|
|
2
|
+
import { isBexioOAuthError } from "./errors.js";
|
|
3
|
+
import { lockWindow, refreshLocked } from "./refresh.js";
|
|
4
|
+
export const DEFAULT_IDLE_LIMIT = 100;
|
|
5
|
+
/** `BexioOAuth.refreshIdle`: refreshes idle connections one after another. */
|
|
6
|
+
export async function refreshIdle(ctx, options) {
|
|
7
|
+
const { store, clock } = ctx;
|
|
8
|
+
const idleMs = toMs(options?.idleFor, "idleFor");
|
|
9
|
+
const budgetMs = options.timeBudget === undefined
|
|
10
|
+
? Infinity
|
|
11
|
+
: toMs(options.timeBudget, "timeBudget");
|
|
12
|
+
const startedAt = clock().getTime();
|
|
13
|
+
const result = {
|
|
14
|
+
refreshed: 0,
|
|
15
|
+
skipped: 0,
|
|
16
|
+
reauthRequired: 0,
|
|
17
|
+
failed: 0,
|
|
18
|
+
remaining: 0,
|
|
19
|
+
};
|
|
20
|
+
const refreshedBefore = new Date(startedAt - idleMs);
|
|
21
|
+
const idle = await store.listIdleConnections({
|
|
22
|
+
refreshedBefore,
|
|
23
|
+
limit: options.limit ?? DEFAULT_IDLE_LIMIT,
|
|
24
|
+
});
|
|
25
|
+
if (idle.length === 0)
|
|
26
|
+
return result;
|
|
27
|
+
try {
|
|
28
|
+
await ctx.oidc.config();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Bexio unreachable: report, don't throw; the next run retries.
|
|
32
|
+
result.failed = idle.length;
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
for (const [index, connection] of idle.entries()) {
|
|
36
|
+
// Stop before taking the next lock, so a function timeout can't cut a
|
|
37
|
+
// refresh in half. The oldest connections come first, so the next run
|
|
38
|
+
// continues where this one stopped.
|
|
39
|
+
if (clock().getTime() - startedAt >= budgetMs) {
|
|
40
|
+
result.remaining = idle.length - index;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const window = lockWindow(ctx, clock());
|
|
44
|
+
const locked = await store.acquireRefreshLock({
|
|
45
|
+
id: connection.id,
|
|
46
|
+
...window,
|
|
47
|
+
refreshedBefore,
|
|
48
|
+
});
|
|
49
|
+
if (!locked) {
|
|
50
|
+
result.skipped++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (await refreshLocked(ctx, locked, window.lockToken))
|
|
55
|
+
result.refreshed++;
|
|
56
|
+
else
|
|
57
|
+
result.skipped++;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isBexioOAuthError(error, "reauth_required")) {
|
|
61
|
+
result.reauthRequired++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result.failed++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=idle-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-sweep.js","sourceRoot":"","sources":["../src/idle-sweep.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAiB,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAkB,EAClB,OAAqE;IAErE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACjD,MAAM,QAAQ,GACZ,OAAO,CAAC,UAAU,KAAK,SAAS;QAC9B,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC;IACpC,MAAM,MAAM,GAAsB;QAChC,SAAS,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;QACV,cAAc,EAAE,CAAC;QACjB,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,CAAC;KACb,CAAC;IACF,MAAM,eAAe,GAAG,IAAI,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,mBAAmB,CAAC;QAC3C,eAAe;QACf,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,kBAAkB;KAC3C,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;QAChE,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACjD,sEAAsE;QACtE,sEAAsE;QACtE,oCAAoC;QACpC,IAAI,KAAK,EAAE,CAAC,OAAO,EAAE,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC;YAC9C,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;YACvC,MAAM;QACR,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC;YAC5C,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,GAAG,MAAM;YACT,eAAe;SAChB,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,IAAI,MAAM,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;gBACpD,MAAM,CAAC,SAAS,EAAE,CAAC;;gBAChB,MAAM,CAAC,OAAO,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,CAAC,EAAE,CAAC;gBAChD,MAAM,CAAC,cAAc,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createBexioOAuth
|
|
1
|
+
export { createBexioOAuth } from "./client.js";
|
|
2
|
+
export type { AccessToken, BexioOAuth, BexioOAuthConfig, CallbackOptions, CallbackResult, ExternalTokens, Grant, GrantSelector, RefreshIdleResult, } from "./types.js";
|
|
2
3
|
export { BASE_SCOPES, BEXIO_API_BASE_URL, BEXIO_ISSUER, mapClaims, type BexioIdentity, } from "./claims.js";
|
|
3
4
|
export { generateKey, type Keyring } from "./crypto.js";
|
|
4
5
|
export type { Duration } from "./duration.js";
|
|
@@ -7,5 +8,5 @@ export { roleFromPermissions, type Role } from "./permissions.js";
|
|
|
7
8
|
export { migrations, type Migration } from "./sql.js";
|
|
8
9
|
export { memoryStore } from "./store/memory.js";
|
|
9
10
|
export { postgresStore, type SqlClient } from "./store/postgres.js";
|
|
10
|
-
export type {
|
|
11
|
+
export type { ConnectionRecord, ConnectionRefresh, GrantRecord, GrantStatus, GrantStore, GrantUpsert, LockRequest, } from "./store/types.js";
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,cAAc,EACd,KAAK,EACL,aAAa,EACb,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createBexioOAuth
|
|
1
|
+
export { createBexioOAuth } from "./client.js";
|
|
2
2
|
export { BASE_SCOPES, BEXIO_API_BASE_URL, BEXIO_ISSUER, mapClaims, } from "./claims.js";
|
|
3
3
|
export { generateKey } from "./crypto.js";
|
|
4
4
|
export { BexioOAuthError, isBexioOAuthError, } from "./errors.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAY/C,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,YAAY,EACZ,SAAS,GAEV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AAExD,OAAO,EACL,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAa,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAkB,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAkB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.generated.d.ts","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EAsD1C,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const MIGRATIONS = [
|
|
2
|
+
{
|
|
3
|
+
number: 1,
|
|
4
|
+
name: "0001_bexio_oauth.sql",
|
|
5
|
+
sql: `-- @soulcode/bexio-oauth 0001: connections and grants.
|
|
6
|
+
-- Token columns hold JWE ciphertext; only your server-side database role
|
|
7
|
+
-- should be able to read these tables.
|
|
8
|
+
|
|
9
|
+
-- One row per Bexio identity (company, user): the tokens of its one refresh
|
|
10
|
+
-- token chain, shared by every owner that connected this identity.
|
|
11
|
+
CREATE TABLE IF NOT EXISTS bexio_connections (
|
|
12
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
13
|
+
bexio_company_id text NOT NULL,
|
|
14
|
+
bexio_user_id text NOT NULL,
|
|
15
|
+
bexio_company_user_id integer,
|
|
16
|
+
company_name text,
|
|
17
|
+
user_name text,
|
|
18
|
+
user_email text,
|
|
19
|
+
role text NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
|
|
20
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'reauth_required')),
|
|
21
|
+
access_token text NOT NULL,
|
|
22
|
+
refresh_token text,
|
|
23
|
+
access_expires_at timestamptz,
|
|
24
|
+
refresh_expires_at timestamptz,
|
|
25
|
+
scope text,
|
|
26
|
+
refresh_locked_until timestamptz,
|
|
27
|
+
refresh_lock_token text,
|
|
28
|
+
authorized_at timestamptz NOT NULL,
|
|
29
|
+
last_refreshed_at timestamptz NOT NULL DEFAULT now(),
|
|
30
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
31
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
32
|
+
CONSTRAINT bexio_connections_company_user_key UNIQUE (bexio_company_id, bexio_user_id)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- refreshIdle(): active connections by last refresh.
|
|
36
|
+
CREATE INDEX IF NOT EXISTS bexio_connections_idle_idx
|
|
37
|
+
ON bexio_connections (last_refreshed_at)
|
|
38
|
+
WHERE status = 'active';
|
|
39
|
+
|
|
40
|
+
-- One row per (owner, connection): an owner's access to a connection. A
|
|
41
|
+
-- connection is deleted only once no grant references it (RESTRICT).
|
|
42
|
+
CREATE TABLE IF NOT EXISTS bexio_grants (
|
|
43
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
44
|
+
owner_id text NOT NULL,
|
|
45
|
+
connection_id uuid NOT NULL REFERENCES bexio_connections (id) ON DELETE RESTRICT,
|
|
46
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
47
|
+
CONSTRAINT bexio_grants_owner_connection_key UNIQUE (owner_id, connection_id)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- Disconnect checks whether a connection has grants left.
|
|
51
|
+
CREATE INDEX IF NOT EXISTS bexio_grants_connection_idx
|
|
52
|
+
ON bexio_grants (connection_id);
|
|
53
|
+
`,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
//# sourceMappingURL=migrations.generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.generated.js","sourceRoot":"","sources":["../src/migrations.generated.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,UAAU,GAAyB;IAC9C;QACE,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,sBAAsB;QAC5B,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDR;KACE;CACF,CAAC"}
|
package/dist/oidc.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { createRemoteJWKSet } from "jose";
|
|
3
|
+
/** Per HTTP request to Bexio. Kept well below the default refresh lock TTL. */
|
|
4
|
+
export declare const HTTP_TIMEOUT_SECONDS = 10;
|
|
5
|
+
/**
|
|
6
|
+
* A safe summary of an upstream error for `BexioOAuthError.cause`: the OAuth
|
|
7
|
+
* error code and HTTP status, never response bodies or token values.
|
|
8
|
+
*/
|
|
9
|
+
export declare function describe(error: unknown): {
|
|
10
|
+
name: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
status?: number;
|
|
13
|
+
};
|
|
14
|
+
export type KeySet = ReturnType<typeof createRemoteJWKSet>;
|
|
15
|
+
/** Bexio's OIDC metadata and signing keys, loaded lazily. */
|
|
16
|
+
export interface Oidc {
|
|
17
|
+
/** The discovered configuration; a failed discovery is retried on the next call. */
|
|
18
|
+
config(): Promise<oidc.Configuration>;
|
|
19
|
+
/** Bexio's signing keys (cached, refetched on an unknown key id). */
|
|
20
|
+
keySet(): Promise<KeySet>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createOidc(options: {
|
|
23
|
+
issuer: string;
|
|
24
|
+
clientId: string;
|
|
25
|
+
clientSecret: string;
|
|
26
|
+
/** Allow plain http (local mocks only). */
|
|
27
|
+
allowInsecure: boolean;
|
|
28
|
+
fetchImpl: typeof fetch;
|
|
29
|
+
}): Oidc;
|
|
30
|
+
//# sourceMappingURL=oidc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc.d.ts","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,kBAAkB,EAAkC,MAAM,MAAM,CAAC;AAG1E,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAeA;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE3D,6DAA6D;AAC7D,MAAM,WAAW,IAAI;IACnB,oFAAoF;IACpF,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACtC,qEAAqE;IACrE,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,KAAK,CAAC;CACzB,GAAG,IAAI,CA+CP"}
|
package/dist/oidc.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { createRemoteJWKSet, customFetch as joseCustomFetch } from "jose";
|
|
3
|
+
import { BexioOAuthError } from "./errors.js";
|
|
4
|
+
/** Per HTTP request to Bexio. Kept well below the default refresh lock TTL. */
|
|
5
|
+
export const HTTP_TIMEOUT_SECONDS = 10;
|
|
6
|
+
/**
|
|
7
|
+
* A safe summary of an upstream error for `BexioOAuthError.cause`: the OAuth
|
|
8
|
+
* error code and HTTP status, never response bodies or token values.
|
|
9
|
+
*/
|
|
10
|
+
export function describe(error) {
|
|
11
|
+
if (error instanceof oidc.ResponseBodyError) {
|
|
12
|
+
return { name: error.name, error: error.error, status: error.status };
|
|
13
|
+
}
|
|
14
|
+
if (error instanceof oidc.AuthorizationResponseError) {
|
|
15
|
+
return { name: error.name, error: error.error };
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
const code = error.code;
|
|
19
|
+
return {
|
|
20
|
+
name: error.name,
|
|
21
|
+
...(typeof code === "string" ? { error: code } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { name: "unknown" };
|
|
25
|
+
}
|
|
26
|
+
export function createOidc(options) {
|
|
27
|
+
const { issuer, clientId, clientSecret, allowInsecure, fetchImpl } = options;
|
|
28
|
+
const issuerUrl = new URL(issuer);
|
|
29
|
+
const customFetch = (url, init) => fetchImpl(url, init);
|
|
30
|
+
let discovered;
|
|
31
|
+
function config() {
|
|
32
|
+
discovered ??= oidc
|
|
33
|
+
.discovery(issuerUrl, clientId, clientSecret, undefined, {
|
|
34
|
+
execute: allowInsecure ? [oidc.allowInsecureRequests] : [],
|
|
35
|
+
// Well below the refresh lock TTL, so a slow IdP can't outlive the lock.
|
|
36
|
+
timeout: HTTP_TIMEOUT_SECONDS,
|
|
37
|
+
[oidc.customFetch]: customFetch,
|
|
38
|
+
})
|
|
39
|
+
.then((c) => {
|
|
40
|
+
c[oidc.customFetch] = customFetch;
|
|
41
|
+
oidc.enableNonRepudiationChecks(c);
|
|
42
|
+
return c;
|
|
43
|
+
})
|
|
44
|
+
.catch((cause) => {
|
|
45
|
+
discovered = undefined;
|
|
46
|
+
throw new BexioOAuthError("idp_error", `OIDC discovery failed for ${issuer}`, { cause: describe(cause) });
|
|
47
|
+
});
|
|
48
|
+
return discovered;
|
|
49
|
+
}
|
|
50
|
+
// Cached, refetched on an unknown key id, with a timeout and a cooldown so a
|
|
51
|
+
// bad token can't hammer the issuer.
|
|
52
|
+
let jwks;
|
|
53
|
+
async function keySet() {
|
|
54
|
+
if (!jwks) {
|
|
55
|
+
const uri = (await config()).serverMetadata().jwks_uri;
|
|
56
|
+
if (!uri)
|
|
57
|
+
throw new BexioOAuthError("idp_error", "issuer publishes no jwks_uri");
|
|
58
|
+
jwks = createRemoteJWKSet(new URL(uri), {
|
|
59
|
+
timeoutDuration: HTTP_TIMEOUT_SECONDS * 1000,
|
|
60
|
+
[joseCustomFetch]: fetchImpl,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return jwks;
|
|
64
|
+
}
|
|
65
|
+
return { config, keySet };
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=oidc.js.map
|
package/dist/oidc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc.js","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import type { ClientContext } from "./config.js";
|
|
3
|
+
import type { ConnectionRecord } from "./store/types.js";
|
|
4
|
+
import type { AccessToken, GrantSelector } from "./types.js";
|
|
5
|
+
/** Refresh a little before the access token really expires. */
|
|
6
|
+
export declare const EXPIRY_SKEW_MS = 30000;
|
|
7
|
+
export type TokenResponse = oidc.TokenEndpointResponse & oidc.TokenEndpointResponseHelpers;
|
|
8
|
+
/** Absolute expiries from a token response's `expires_in` / `refresh_expires_in`. */
|
|
9
|
+
export declare function tokenExpiry(tokens: TokenResponse, now: Date): {
|
|
10
|
+
accessExpiresAt: Date | null;
|
|
11
|
+
refreshExpiresAt: Date | null;
|
|
12
|
+
};
|
|
13
|
+
/** True while the access token is valid for longer than the skew. */
|
|
14
|
+
export declare function isFresh(connection: ConnectionRecord, now: Date): boolean;
|
|
15
|
+
/** A new lock holder's token and lease, starting at `now`. */
|
|
16
|
+
export declare function lockWindow(ctx: ClientContext, now: Date): {
|
|
17
|
+
now: Date;
|
|
18
|
+
lockUntil: Date;
|
|
19
|
+
lockToken: `${string}-${string}-${string}-${string}-${string}`;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Refreshes a connection whose lock the caller holds (`lockToken`). Every
|
|
23
|
+
* write is fenced by the token: if the lock was lost (expired and taken over,
|
|
24
|
+
* or the connection was reconnected meanwhile), the write changes nothing and
|
|
25
|
+
* this returns `null`: the caller re-reads the connection.
|
|
26
|
+
*/
|
|
27
|
+
export declare function refreshLocked(ctx: ClientContext, connection: ConnectionRecord, lockToken: string): Promise<ConnectionRecord | null>;
|
|
28
|
+
/** `BexioOAuth.getAccessToken`. */
|
|
29
|
+
export declare function getAccessToken(ctx: ClientContext, selector: GrantSelector): Promise<AccessToken>;
|
|
30
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../src/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE7D,+DAA+D;AAC/D,eAAO,MAAM,cAAc,QAAS,CAAC;AAIrC,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,GACpD,IAAI,CAAC,4BAA4B,CAAC;AAEpC,qFAAqF;AACrF,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,IAAI,GACR;IAAE,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IAAC,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAA;CAAE,CAajE;AAED,qEAAqE;AACrE,wBAAgB,OAAO,CAAC,UAAU,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAKxE;AAED,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI;;;;EAMvD;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,UAAU,EAAE,gBAAgB,EAC5B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoElC;AAID,mCAAmC;AACnC,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,WAAW,CAAC,CAqDtB"}
|