@synqauth/js 0.0.0-stage → 0.0.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 +39 -2
- package/dist/api-Cf9K1Rc9.js +163 -0
- package/dist/index.d.ts +2055 -0
- package/dist/index.js +605 -0
- package/dist/management-y9-9AIc-.d.ts +11198 -0
- package/dist/server.d.ts +175 -0
- package/dist/server.js +197 -0
- package/package.json +46 -3
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { c as SynqError, l as isSynqError, r as paths, t as components } from "./management-y9-9AIc-.js";
|
|
2
|
+
import { JWTPayload, JWTVerifyGetKey } from "jose";
|
|
3
|
+
//#region src/api.d.ts
|
|
4
|
+
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
5
|
+
/** The paths of a contract that answer `M`. */
|
|
6
|
+
type PathsWith<Paths, M extends HttpMethod> = { [P in keyof Paths]: Paths[P] extends { readonly [K in M]: infer O; } ? [O] extends [never] ? never : P : never; }[keyof Paths] & string;
|
|
7
|
+
type Operation<Paths, P extends keyof Paths, M extends HttpMethod> = Paths[P] extends { readonly [K in M]: infer O; } ? O : never;
|
|
8
|
+
type PathParams<O> = O extends {
|
|
9
|
+
readonly parameters: {
|
|
10
|
+
readonly path: infer X;
|
|
11
|
+
};
|
|
12
|
+
} ? X : never;
|
|
13
|
+
type QueryParams<O> = O extends {
|
|
14
|
+
readonly parameters: {
|
|
15
|
+
readonly query?: infer X;
|
|
16
|
+
};
|
|
17
|
+
} ? Exclude<X, undefined> : never;
|
|
18
|
+
type JsonBody<O> = O extends {
|
|
19
|
+
readonly requestBody: {
|
|
20
|
+
readonly content: {
|
|
21
|
+
readonly 'application/json': infer B;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
} ? B : never;
|
|
25
|
+
type OptionalJsonBody<O> = O extends {
|
|
26
|
+
readonly requestBody?: {
|
|
27
|
+
readonly content: {
|
|
28
|
+
readonly 'application/json': infer B;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
} ? B : never;
|
|
32
|
+
/** What a successful call answers: the JSON of its 2xx responses, or nothing for a 204. */
|
|
33
|
+
type Success<O> = O extends {
|
|
34
|
+
readonly responses: infer R;
|
|
35
|
+
} ? { [S in keyof R]: S extends 200 | 201 | 202 | 203 ? R[S] extends {
|
|
36
|
+
readonly content: {
|
|
37
|
+
readonly 'application/json': infer J;
|
|
38
|
+
};
|
|
39
|
+
} ? J : never : S extends 204 ? undefined : never; }[keyof R] : never;
|
|
40
|
+
type Common = {
|
|
41
|
+
/** Sent as `Idempotency-Key`: a POST with the same key runs once (the management API keeps the answer 24 hours). */
|
|
42
|
+
idempotencyKey?: string;
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
};
|
|
45
|
+
/** The options of one call: its path parameters, query and body, as the contract has them. */
|
|
46
|
+
type RequestOptions<O> = ([PathParams<O>] extends [never] ? {
|
|
47
|
+
path?: undefined;
|
|
48
|
+
} : {
|
|
49
|
+
path: PathParams<O>;
|
|
50
|
+
}) & ([QueryParams<O>] extends [never] ? {
|
|
51
|
+
query?: undefined;
|
|
52
|
+
} : {
|
|
53
|
+
query?: QueryParams<O>;
|
|
54
|
+
}) & ([JsonBody<O>] extends [never] ? [OptionalJsonBody<O>] extends [never] ? {
|
|
55
|
+
body?: undefined;
|
|
56
|
+
} : {
|
|
57
|
+
body?: OptionalJsonBody<O>;
|
|
58
|
+
} : {
|
|
59
|
+
body: JsonBody<O>;
|
|
60
|
+
}) & Common;
|
|
61
|
+
type Args<O> = object extends RequestOptions<O> ? [options?: RequestOptions<O>] : [options: RequestOptions<O>];
|
|
62
|
+
type Call<Paths, M extends HttpMethod> = <P extends PathsWith<Paths, M>>(path: P, ...args: Args<Operation<Paths, P, M>>) => Promise<Success<Operation<Paths, P, M>>>;
|
|
63
|
+
/** A client for one Synq contract: one method per HTTP method, each typed by the contract. */
|
|
64
|
+
interface ApiClient<Paths> {
|
|
65
|
+
get: Call<Paths, 'get'>;
|
|
66
|
+
post: Call<Paths, 'post'>;
|
|
67
|
+
put: Call<Paths, 'put'>;
|
|
68
|
+
patch: Call<Paths, 'patch'>;
|
|
69
|
+
delete: Call<Paths, 'delete'>;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/server.d.ts
|
|
73
|
+
/** What an API checks every access token against. */
|
|
74
|
+
export interface AccessTokenVerifierOptions {
|
|
75
|
+
/** The brand's issuer URL, as the dashboard shows it. */
|
|
76
|
+
issuer: string;
|
|
77
|
+
/** This API's identifier, as registered with Synq: the token's `aud`. */
|
|
78
|
+
audience: string;
|
|
79
|
+
/** The algorithms accepted: Synq signs API tokens with ES256. */
|
|
80
|
+
algorithms?: string[];
|
|
81
|
+
/** Seconds of clock difference forgiven on `exp`, `iat` and `nbf`: 5 by default. */
|
|
82
|
+
clockToleranceSeconds?: number;
|
|
83
|
+
fetch?: typeof fetch;
|
|
84
|
+
/** The keys to check with, instead of fetching the issuer's JWKS (for tests). */
|
|
85
|
+
keys?: JWTVerifyGetKey;
|
|
86
|
+
}
|
|
87
|
+
/** What one route wants of a token, on top of the verifier's checks. */
|
|
88
|
+
export interface VerifyAccessTokenOptions {
|
|
89
|
+
/** Scopes the token must carry, every one. */
|
|
90
|
+
scopes?: readonly string[];
|
|
91
|
+
/** The apps (client_ids) whose tokens this route takes (`azp`). */
|
|
92
|
+
authorizedParties?: readonly string[];
|
|
93
|
+
/** The longest ago, in seconds, the person may have signed in (`auth_time`): for sensitive actions. */
|
|
94
|
+
maxAuthAgeSeconds?: number;
|
|
95
|
+
}
|
|
96
|
+
/** A Synq access token's claims (RFC 9068), once checked. */
|
|
97
|
+
export interface AccessTokenClaims {
|
|
98
|
+
readonly iss: string;
|
|
99
|
+
/** The person (`usr_…`), or the machine app's client_id for client credentials. */
|
|
100
|
+
readonly sub: string;
|
|
101
|
+
readonly aud: string | string[];
|
|
102
|
+
/** The app the token was issued to. */
|
|
103
|
+
readonly azp: string | undefined;
|
|
104
|
+
readonly scopes: readonly string[];
|
|
105
|
+
readonly iat: number;
|
|
106
|
+
readonly exp: number;
|
|
107
|
+
readonly auth_time: number | undefined;
|
|
108
|
+
readonly acr: string | undefined;
|
|
109
|
+
readonly amr: readonly string[] | undefined;
|
|
110
|
+
/** An agent acting for the person (device flow): its client_id. */
|
|
111
|
+
readonly act: {
|
|
112
|
+
readonly type?: string;
|
|
113
|
+
readonly client_id?: string;
|
|
114
|
+
} | undefined;
|
|
115
|
+
/** Every claim, as the token carries it. */
|
|
116
|
+
readonly payload: JWTPayload;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Checks one access token for your API, as `createAccessTokenVerifier` makes
|
|
120
|
+
* it: the claims when the token is good, else a `SynqError`
|
|
121
|
+
* (`invalid_token`, `insufficient_scope` or `fresh_sign_in_required`).
|
|
122
|
+
*/
|
|
123
|
+
export type AccessTokenVerifier = (token: string, options?: VerifyAccessTokenOptions) => Promise<AccessTokenClaims>;
|
|
124
|
+
/**
|
|
125
|
+
* A verifier for one API's access tokens. It reads the issuer's JWKS once
|
|
126
|
+
* and keeps it, fetching it again when a token names a key it doesn't have
|
|
127
|
+
* (a rotation), at most every 30 seconds. A discovery that failed is retried
|
|
128
|
+
* after 30 seconds, not on every token. Keep one per API, not one per
|
|
129
|
+
* request. The issuer and its JWKS must be https (http only on this machine).
|
|
130
|
+
*/
|
|
131
|
+
export declare function createAccessTokenVerifier(options: AccessTokenVerifierOptions): AccessTokenVerifier;
|
|
132
|
+
/** A Synq webhook's event: one row of the org's audit log (the management API's `WebhookEvent`). */
|
|
133
|
+
export type WebhookEvent = components['schemas']['WebhookEvent'];
|
|
134
|
+
/** A delivery that passed its check. */
|
|
135
|
+
export interface VerifiedWebhook {
|
|
136
|
+
/** The delivery's `webhook-id`: the same on every retry, so a receiver drops one it has seen. */
|
|
137
|
+
readonly id: string;
|
|
138
|
+
/** When Synq signed it. */
|
|
139
|
+
readonly timestamp: Date;
|
|
140
|
+
readonly event: WebhookEvent;
|
|
141
|
+
}
|
|
142
|
+
type HeadersLike = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
143
|
+
/**
|
|
144
|
+
* Checks a delivery's Standard Webhooks signature (`webhook-id`,
|
|
145
|
+
* `webhook-timestamp`, `webhook-signature`, HMAC-SHA256 over
|
|
146
|
+
* `<id>.<timestamp>.<body>`) against the subscription's `whsec_` secret, and
|
|
147
|
+
* that Synq signed it within `toleranceSeconds` (5 minutes by default), so a
|
|
148
|
+
* captured delivery can't be replayed later. Pass both secrets while one is
|
|
149
|
+
* being rotated: Synq signs with both for 24 hours. `body` is the request's
|
|
150
|
+
* raw body, exactly as it arrived.
|
|
151
|
+
*/
|
|
152
|
+
export declare function verifyWebhook(body: string, headers: HeadersLike, secrets: string | readonly string[], options?: {
|
|
153
|
+
toleranceSeconds?: number;
|
|
154
|
+
now?: Date;
|
|
155
|
+
}): Promise<VerifiedWebhook>;
|
|
156
|
+
/** Who a management API client acts as. */
|
|
157
|
+
export interface ManagementClientOptions {
|
|
158
|
+
/** The management API's origin, as the dashboard shows it. */
|
|
159
|
+
baseUrl: string;
|
|
160
|
+
/** An org API key (`sk_live_…` or `sk_test_…`): it acts in its own org with the permissions it was given. */
|
|
161
|
+
apiKey?: string;
|
|
162
|
+
/** Or a Synq access token, acting as that person: a function so it can be refreshed. */
|
|
163
|
+
accessToken?: () => string | Promise<string>;
|
|
164
|
+
fetch?: typeof fetch;
|
|
165
|
+
}
|
|
166
|
+
/** The management API's typed client: every path, parameter, body and answer from its contract. */
|
|
167
|
+
export type ManagementClient = ApiClient<paths>;
|
|
168
|
+
/**
|
|
169
|
+
* A client for Synq's management API (`/v1`), typed by its contract. Server
|
|
170
|
+
* only: an API key never reaches a browser. Failures throw a `SynqError`
|
|
171
|
+
* with the problem's `code`, such as `insufficient_permissions`.
|
|
172
|
+
*/
|
|
173
|
+
export declare function createManagementClient(options: ManagementClientOptions): ManagementClient;
|
|
174
|
+
//#endregion
|
|
175
|
+
export { type components as ManagementComponents, type paths as ManagementPaths, SynqError, isSynqError };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { i as isSynqError, n as secureUrl, r as SynqError, t as createApiClient } from "./api-Cf9K1Rc9.js";
|
|
2
|
+
import { createRemoteJWKSet, customFetch, errors, jwtVerify } from "jose";
|
|
3
|
+
//#region src/server.ts
|
|
4
|
+
/**
|
|
5
|
+
* A verifier for one API's access tokens. It reads the issuer's JWKS once
|
|
6
|
+
* and keeps it, fetching it again when a token names a key it doesn't have
|
|
7
|
+
* (a rotation), at most every 30 seconds. A discovery that failed is retried
|
|
8
|
+
* after 30 seconds, not on every token. Keep one per API, not one per
|
|
9
|
+
* request. The issuer and its JWKS must be https (http only on this machine).
|
|
10
|
+
*/
|
|
11
|
+
function createAccessTokenVerifier(options) {
|
|
12
|
+
const issuer = secureUrl(options.issuer, "The issuer").href.replace(/\/+$/, "");
|
|
13
|
+
let keys = options.keys ? Promise.resolve(options.keys) : void 0;
|
|
14
|
+
/** When discovery last failed: it isn't asked again for DISCOVERY_RETRY_MS. */
|
|
15
|
+
let failedAt = 0;
|
|
16
|
+
const getKeys = () => {
|
|
17
|
+
if (!keys && Date.now() - failedAt < DISCOVERY_RETRY_MS) return Promise.reject(new SynqError("discovery_failed", `Synq's discovery document at ${issuer} couldn't be read`, { description: "It failed moments ago; it is asked again 30 seconds after." }));
|
|
18
|
+
keys ??= discoverKeys(issuer, options.fetch).catch((error) => {
|
|
19
|
+
keys = void 0;
|
|
20
|
+
failedAt = Date.now();
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
return keys;
|
|
24
|
+
};
|
|
25
|
+
return async (token, verify = {}) => {
|
|
26
|
+
let payload;
|
|
27
|
+
try {
|
|
28
|
+
({payload} = await jwtVerify(token, await getKeys(), {
|
|
29
|
+
issuer,
|
|
30
|
+
audience: options.audience,
|
|
31
|
+
algorithms: options.algorithms ?? ["ES256"],
|
|
32
|
+
typ: "at+jwt",
|
|
33
|
+
clockTolerance: options.clockToleranceSeconds ?? 5,
|
|
34
|
+
requiredClaims: [
|
|
35
|
+
"sub",
|
|
36
|
+
"iat",
|
|
37
|
+
"exp"
|
|
38
|
+
]
|
|
39
|
+
}));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw tokenError(error);
|
|
42
|
+
}
|
|
43
|
+
const claims = accessTokenClaims(payload);
|
|
44
|
+
if (verify.authorizedParties && !verify.authorizedParties.includes(claims.azp ?? "")) throw new SynqError("invalid_token", "The token was issued to an app this route takes none from", { status: 401 });
|
|
45
|
+
const missing = (verify.scopes ?? []).filter((scope) => !claims.scopes.includes(scope));
|
|
46
|
+
if (missing.length > 0) throw new SynqError("insufficient_scope", `The token lacks ${missing.join(" ")}`, {
|
|
47
|
+
status: 403,
|
|
48
|
+
description: missing.join(" ")
|
|
49
|
+
});
|
|
50
|
+
if (verify.maxAuthAgeSeconds !== void 0) {
|
|
51
|
+
const signedInAt = provenSignInTime(claims);
|
|
52
|
+
if ((signedInAt === void 0 ? Infinity : Date.now() / 1e3 - signedInAt) > verify.maxAuthAgeSeconds) throw new SynqError("fresh_sign_in_required", `This wants a sign-in from the last ${verify.maxAuthAgeSeconds} seconds: sign in again with max_age`, { status: 403 });
|
|
53
|
+
}
|
|
54
|
+
return claims;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** How long a failed discovery is remembered before it is asked again. */
|
|
58
|
+
const DISCOVERY_RETRY_MS = 3e4;
|
|
59
|
+
/**
|
|
60
|
+
* When the person signed in, for `maxAuthAgeSeconds`: the token's
|
|
61
|
+
* `auth_time`, or `undefined` when it has none, which fails a recency check
|
|
62
|
+
* closed. Synq itself goes further on its own routes: a sign-in whose time it
|
|
63
|
+
* never saw proved (an upstream provider that says nothing of when, such as
|
|
64
|
+
* Discord) is tracked on the issuer and never counts as fresh there. Tokens
|
|
65
|
+
* carry no mark of it, so an API's own check here is `auth_time` alone; an
|
|
66
|
+
* API that needs the stronger rule sends the person through Synq's
|
|
67
|
+
* `max_age` sign-in.
|
|
68
|
+
*/
|
|
69
|
+
function provenSignInTime(claims) {
|
|
70
|
+
return claims.auth_time;
|
|
71
|
+
}
|
|
72
|
+
async function discoverKeys(issuer, fetcher) {
|
|
73
|
+
let jwksUri;
|
|
74
|
+
try {
|
|
75
|
+
const response = await (fetcher ?? fetch)(`${issuer}/.well-known/openid-configuration`);
|
|
76
|
+
if (!response.ok) throw new Error(`discovery answered ${response.status}`);
|
|
77
|
+
const metadata = await response.json();
|
|
78
|
+
if (metadata.issuer !== issuer || typeof metadata.jwks_uri !== "string") throw new Error("the discovery document names another issuer, or no JWKS");
|
|
79
|
+
jwksUri = secureUrl(metadata.jwks_uri, "The issuer's jwks_uri").href;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new SynqError("discovery_failed", `Synq's discovery document at ${issuer} couldn't be read`, { cause: error });
|
|
82
|
+
}
|
|
83
|
+
return createRemoteJWKSet(new URL(jwksUri), {
|
|
84
|
+
cooldownDuration: 3e4,
|
|
85
|
+
cacheMaxAge: 6e5,
|
|
86
|
+
...fetcher ? { [customFetch]: (url, init) => fetcher(url, init) } : {}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function accessTokenClaims(payload) {
|
|
90
|
+
const string = (value) => typeof value === "string" ? value : void 0;
|
|
91
|
+
const number = (value) => typeof value === "number" ? value : void 0;
|
|
92
|
+
const scope = string(payload.scope);
|
|
93
|
+
const amr = Array.isArray(payload.amr) ? payload.amr.filter((v) => typeof v === "string") : void 0;
|
|
94
|
+
const act = payload.act !== null && typeof payload.act === "object" ? payload.act : void 0;
|
|
95
|
+
return {
|
|
96
|
+
iss: payload.iss,
|
|
97
|
+
sub: payload.sub,
|
|
98
|
+
aud: payload.aud,
|
|
99
|
+
azp: string(payload.azp) ?? string(payload.client_id),
|
|
100
|
+
scopes: scope ? scope.split(" ").filter(Boolean) : [],
|
|
101
|
+
iat: payload.iat,
|
|
102
|
+
exp: payload.exp,
|
|
103
|
+
auth_time: number(payload.auth_time),
|
|
104
|
+
acr: string(payload.acr),
|
|
105
|
+
amr,
|
|
106
|
+
act,
|
|
107
|
+
payload
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function tokenError(error) {
|
|
111
|
+
if (error instanceof SynqError) return error;
|
|
112
|
+
if (error instanceof errors.JWTExpired) return new SynqError("token_expired", "The access token has expired", {
|
|
113
|
+
status: 401,
|
|
114
|
+
cause: error
|
|
115
|
+
});
|
|
116
|
+
if (error instanceof errors.JWKSTimeout || error instanceof TypeError) return new SynqError("network_error", "Synq's keys couldn't be fetched", {
|
|
117
|
+
status: 503,
|
|
118
|
+
cause: error
|
|
119
|
+
});
|
|
120
|
+
return new SynqError("invalid_token", "The access token is not valid for this API", {
|
|
121
|
+
status: 401,
|
|
122
|
+
cause: error
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Checks a delivery's Standard Webhooks signature (`webhook-id`,
|
|
127
|
+
* `webhook-timestamp`, `webhook-signature`, HMAC-SHA256 over
|
|
128
|
+
* `<id>.<timestamp>.<body>`) against the subscription's `whsec_` secret, and
|
|
129
|
+
* that Synq signed it within `toleranceSeconds` (5 minutes by default), so a
|
|
130
|
+
* captured delivery can't be replayed later. Pass both secrets while one is
|
|
131
|
+
* being rotated: Synq signs with both for 24 hours. `body` is the request's
|
|
132
|
+
* raw body, exactly as it arrived.
|
|
133
|
+
*/
|
|
134
|
+
async function verifyWebhook(body, headers, secrets, options = {}) {
|
|
135
|
+
const id = header(headers, "webhook-id");
|
|
136
|
+
const timestamp = header(headers, "webhook-timestamp");
|
|
137
|
+
const signatures = header(headers, "webhook-signature");
|
|
138
|
+
if (!id || !timestamp || !signatures) throw new SynqError("invalid_signature", "The delivery is missing its webhook-* headers");
|
|
139
|
+
const seconds = Number(timestamp);
|
|
140
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3;
|
|
141
|
+
if (!Number.isInteger(seconds) || Math.abs(now - seconds) > (options.toleranceSeconds ?? 300)) throw new SynqError("stale_webhook", "The delivery was signed too long ago, or in the future");
|
|
142
|
+
const message = new TextEncoder().encode(`${id}.${seconds}.${body}`);
|
|
143
|
+
const given = signatures.split(" ").filter((signature) => signature.startsWith("v1,")).map((signature) => fromBase64(signature.slice(3))).filter((bytes) => bytes !== void 0);
|
|
144
|
+
for (const secret of typeof secrets === "string" ? [secrets] : secrets) {
|
|
145
|
+
const raw = fromBase64(secret.replace(/^whsec_/, ""));
|
|
146
|
+
if (!raw) throw new SynqError("invalid_options", "A webhook secret is not a whsec_ secret");
|
|
147
|
+
const key = await crypto.subtle.importKey("raw", raw, {
|
|
148
|
+
name: "HMAC",
|
|
149
|
+
hash: "SHA-256"
|
|
150
|
+
}, false, ["verify"]);
|
|
151
|
+
for (const signature of given) if (await crypto.subtle.verify("HMAC", key, signature, message)) {
|
|
152
|
+
let event;
|
|
153
|
+
try {
|
|
154
|
+
event = JSON.parse(body);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
throw new SynqError("invalid_signature", "The signed body is not JSON", { cause: error });
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
timestamp: /* @__PURE__ */ new Date(seconds * 1e3),
|
|
161
|
+
event
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new SynqError("invalid_signature", "The delivery's signature matches none of the secrets");
|
|
166
|
+
}
|
|
167
|
+
function header(headers, name) {
|
|
168
|
+
if (headers instanceof Headers) return headers.get(name) ?? void 0;
|
|
169
|
+
const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
170
|
+
return Array.isArray(entry) ? entry.join(" ") : entry;
|
|
171
|
+
}
|
|
172
|
+
function fromBase64(value) {
|
|
173
|
+
try {
|
|
174
|
+
const binary = atob(value);
|
|
175
|
+
const bytes = new Uint8Array(binary.length);
|
|
176
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
177
|
+
return bytes;
|
|
178
|
+
} catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A client for Synq's management API (`/v1`), typed by its contract. Server
|
|
184
|
+
* only: an API key never reaches a browser. Failures throw a `SynqError`
|
|
185
|
+
* with the problem's `code`, such as `insufficient_permissions`.
|
|
186
|
+
*/
|
|
187
|
+
function createManagementClient(options) {
|
|
188
|
+
const { apiKey, accessToken } = options;
|
|
189
|
+
if (!apiKey === !accessToken) throw new SynqError("invalid_options", "Give the management client an apiKey or an accessToken");
|
|
190
|
+
return createApiClient({
|
|
191
|
+
baseUrl: options.baseUrl,
|
|
192
|
+
credential: apiKey ? () => apiKey : accessToken,
|
|
193
|
+
fetch: options.fetch
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { SynqError, createAccessTokenVerifier, createManagementClient, isSynqError, verifyWebhook };
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synqauth/js",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Synq's framework-agnostic client: sign-in and sessions in the browser, the account API, and token and webhook verification on your server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/aerosol-build/synq.git",
|
|
9
|
+
"directory": "packages/js"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./server": {
|
|
22
|
+
"types": "./dist/server.d.ts",
|
|
23
|
+
"default": "./dist/server.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
35
|
+
"openapi-typescript": "7.13.0",
|
|
36
|
+
"vitest": "5.0.2"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"oauth4webapi": "3.8.8",
|
|
40
|
+
"jose": "6.2.12"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"generate": "node scripts/generate.ts",
|
|
45
|
+
"lint": "eslint . && node scripts/generate.ts --check",
|
|
46
|
+
"typecheck": "tsc",
|
|
47
|
+
"test": "vitest run --coverage"
|
|
48
|
+
}
|
|
6
49
|
}
|