@mymehq/sdk 4.0.0 → 4.1.1
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/dist/auth/index.d.ts +129 -0
- package/dist/auth/index.js +331 -0
- package/dist/index.d.ts +22 -6
- package/dist/index.js +26 -4
- package/package.json +6 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token storage interface and default implementations.
|
|
3
|
+
*
|
|
4
|
+
* Browser default: localStorage keyed by `(origin, client_id)`.
|
|
5
|
+
* Node default: in-memory; Node consumers can pass a file-backed
|
|
6
|
+
* implementation if they need cross-process persistence.
|
|
7
|
+
*/
|
|
8
|
+
interface TokenStorage {
|
|
9
|
+
get(key: string): Promise<string | null>;
|
|
10
|
+
set(key: string, value: string): Promise<void>;
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
declare class InMemoryTokenStorage implements TokenStorage {
|
|
14
|
+
private map;
|
|
15
|
+
get(key: string): Promise<string | null>;
|
|
16
|
+
set(key: string, value: string): Promise<void>;
|
|
17
|
+
delete(key: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
declare class LocalStorageTokenStorage implements TokenStorage {
|
|
20
|
+
get(key: string): Promise<string | null>;
|
|
21
|
+
set(key: string, value: string): Promise<void>;
|
|
22
|
+
delete(key: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/** Pick the most appropriate default storage for the current runtime. */
|
|
25
|
+
declare function defaultTokenStorage(): TokenStorage;
|
|
26
|
+
|
|
27
|
+
/** TokenProvider gives MymeClient an access token for every request. */
|
|
28
|
+
interface TokenProvider {
|
|
29
|
+
/** Returns a non-expired access token. Refreshes proactively if < 60s
|
|
30
|
+
* remain on the current one. Single-flight: concurrent callers await
|
|
31
|
+
* the same in-flight refresh promise. */
|
|
32
|
+
getAccessToken(): Promise<string>;
|
|
33
|
+
/** Subscribe to sign-out events (fired on invalid_grant /
|
|
34
|
+
* token_reuse_detected). Returns an unsubscribe fn. */
|
|
35
|
+
onSignOut(handler: () => void): () => void;
|
|
36
|
+
/** Force a sign-out — clears local storage, fires onSignOut handlers. */
|
|
37
|
+
signOut(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* MymeAuth — owns the OAuth dance for browser apps signing into Myme.
|
|
42
|
+
*
|
|
43
|
+
* Flow:
|
|
44
|
+
* const auth = new MymeAuth({ issuer, clientId, redirectUri, scopes });
|
|
45
|
+
* // On a "Sign in" click:
|
|
46
|
+
* window.location.href = await auth.buildAuthorizeUrl();
|
|
47
|
+
* // On the callback page:
|
|
48
|
+
* const provider = await auth.handleCallback(window.location.href);
|
|
49
|
+
* const client = new MymeClient({ url: issuer, tokenProvider: provider });
|
|
50
|
+
*
|
|
51
|
+
* Restoration on page load: `await auth.restore()` returns a TokenProvider
|
|
52
|
+
* if a session is in storage, null otherwise.
|
|
53
|
+
*
|
|
54
|
+
* Sign-out: `await auth.signOut(provider)` revokes locally and clears
|
|
55
|
+
* persisted tokens; visible "session expired" UX is the caller's
|
|
56
|
+
* responsibility (subscribe via provider.onSignOut).
|
|
57
|
+
*/
|
|
58
|
+
interface MymeAuthConfig {
|
|
59
|
+
/** Myme server URL — protocol + host (and port). */
|
|
60
|
+
issuer: string;
|
|
61
|
+
/** OAuth client id, registered via POST /auth/clients. */
|
|
62
|
+
clientId: string;
|
|
63
|
+
/** Exact-match redirect URI. Must be registered for this client. */
|
|
64
|
+
redirectUri: string;
|
|
65
|
+
/** Scopes to request (e.g. ["core.note:read", "core.note:write"]). */
|
|
66
|
+
scopes: string[];
|
|
67
|
+
/** Token storage. Defaults to localStorage in browser, in-memory in Node. */
|
|
68
|
+
storage?: TokenStorage;
|
|
69
|
+
/** Override for the global fetch (testing). */
|
|
70
|
+
fetch?: typeof globalThis.fetch;
|
|
71
|
+
}
|
|
72
|
+
declare class MymeAuth {
|
|
73
|
+
private readonly issuer;
|
|
74
|
+
private readonly clientId;
|
|
75
|
+
private readonly redirectUri;
|
|
76
|
+
private readonly scopes;
|
|
77
|
+
private readonly storage;
|
|
78
|
+
private readonly fetch;
|
|
79
|
+
private readonly pendingKey;
|
|
80
|
+
private readonly tokensKey;
|
|
81
|
+
constructor(config: MymeAuthConfig);
|
|
82
|
+
/** Build the authorize URL and persist the PKCE verifier + state. */
|
|
83
|
+
buildAuthorizeUrl(opts?: {
|
|
84
|
+
state?: string;
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Exchange the authorization code for tokens. Call from your `/callback`
|
|
88
|
+
* route handler with `window.location.href` (or the equivalent server-
|
|
89
|
+
* side request URL). Throws OAuthError on failure.
|
|
90
|
+
*/
|
|
91
|
+
handleCallback(callbackUrl: string): Promise<TokenProvider>;
|
|
92
|
+
/** Restore a TokenProvider from persisted storage; null if no session. */
|
|
93
|
+
restore(): Promise<TokenProvider | null>;
|
|
94
|
+
/** Sign out — clears persisted tokens. */
|
|
95
|
+
signOut(provider: TokenProvider): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Typed OAuth error surface. Mirrors the wire `error` codes from
|
|
100
|
+
* RFC 6749 §5.2 and the rotation-replay extension.
|
|
101
|
+
*/
|
|
102
|
+
type OAuthErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "invalid_scope" | "invalid_token" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "access_denied" | "insufficient_scope" | "token_reuse_detected" | "server_error" | "temporarily_unavailable";
|
|
103
|
+
declare class OAuthError extends Error {
|
|
104
|
+
readonly code: OAuthErrorCode;
|
|
105
|
+
readonly status: number;
|
|
106
|
+
constructor(code: OAuthErrorCode, message: string, status?: number);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* PKCE primitives — RFC 7636. Verifier is 43–128 chars of base64url
|
|
111
|
+
* generated from random bytes; challenge is BASE64URL(SHA-256(verifier)).
|
|
112
|
+
*
|
|
113
|
+
* Uses Web Crypto so the auth subpath is universal (browser + Node 15+
|
|
114
|
+
* + Cloudflare Workers + Deno). The SDK's data-plane root path keeps
|
|
115
|
+
* its node:crypto dependency for HMAC webhook verification; nothing
|
|
116
|
+
* Node-only leaks into ./auth.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* Generate a fresh PKCE code verifier. Default length 64 — within RFC's
|
|
120
|
+
* 43–128 range; longer is harder to brute force, shorter speeds up dev
|
|
121
|
+
* tooling. 64 is a balanced default.
|
|
122
|
+
*/
|
|
123
|
+
declare function generateCodeVerifier(length?: number): string;
|
|
124
|
+
/** Compute the S256 challenge: BASE64URL(SHA-256(verifier)). */
|
|
125
|
+
declare function computeCodeChallenge(verifier: string): Promise<string>;
|
|
126
|
+
/** Generate a random opaque state value for CSRF protection on the redirect. */
|
|
127
|
+
declare function generateState(byteLength?: number): string;
|
|
128
|
+
|
|
129
|
+
export { InMemoryTokenStorage, LocalStorageTokenStorage, MymeAuth, type MymeAuthConfig, OAuthError, type OAuthErrorCode, type TokenProvider, type TokenStorage, computeCodeChallenge, defaultTokenStorage, generateCodeVerifier, generateState };
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/auth/storage.ts
|
|
2
|
+
var InMemoryTokenStorage = class {
|
|
3
|
+
map = /* @__PURE__ */ new Map();
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
5
|
+
async get(key) {
|
|
6
|
+
return this.map.get(key) ?? null;
|
|
7
|
+
}
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
9
|
+
async set(key, value) {
|
|
10
|
+
this.map.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
13
|
+
async delete(key) {
|
|
14
|
+
this.map.delete(key);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var LocalStorageTokenStorage = class {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
19
|
+
async get(key) {
|
|
20
|
+
return globalThis.localStorage.getItem(key);
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
23
|
+
async set(key, value) {
|
|
24
|
+
globalThis.localStorage.setItem(key, value);
|
|
25
|
+
}
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
27
|
+
async delete(key) {
|
|
28
|
+
globalThis.localStorage.removeItem(key);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function defaultTokenStorage() {
|
|
32
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
33
|
+
return new LocalStorageTokenStorage();
|
|
34
|
+
}
|
|
35
|
+
return new InMemoryTokenStorage();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/auth/pkce.ts
|
|
39
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
40
|
+
function base64url(bytes) {
|
|
41
|
+
let s = "";
|
|
42
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
43
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
44
|
+
}
|
|
45
|
+
function generateCodeVerifier(length = 64) {
|
|
46
|
+
if (length < 43 || length > 128) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`PKCE verifier length must be 43-128, got ${String(length)}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const bytes = new Uint8Array(length);
|
|
52
|
+
crypto.getRandomValues(bytes);
|
|
53
|
+
let out = "";
|
|
54
|
+
for (const byte of bytes) {
|
|
55
|
+
out += ALPHABET.charAt(byte % ALPHABET.length);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
async function computeCodeChallenge(verifier) {
|
|
60
|
+
const data = new TextEncoder().encode(verifier);
|
|
61
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
62
|
+
return base64url(new Uint8Array(hash));
|
|
63
|
+
}
|
|
64
|
+
function generateState(byteLength = 16) {
|
|
65
|
+
const bytes = new Uint8Array(byteLength);
|
|
66
|
+
crypto.getRandomValues(bytes);
|
|
67
|
+
return base64url(bytes);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/auth/errors.ts
|
|
71
|
+
var OAuthError = class extends Error {
|
|
72
|
+
code;
|
|
73
|
+
status;
|
|
74
|
+
constructor(code, message, status = 400) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "OAuthError";
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.status = status;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/auth/token-provider.ts
|
|
83
|
+
var PROACTIVE_WINDOW_MS = 6e4;
|
|
84
|
+
var StoredTokenProvider = class {
|
|
85
|
+
issuer;
|
|
86
|
+
clientId;
|
|
87
|
+
storage;
|
|
88
|
+
storageKey;
|
|
89
|
+
fetch;
|
|
90
|
+
cache = null;
|
|
91
|
+
inflightRefresh = null;
|
|
92
|
+
signOutHandlers = /* @__PURE__ */ new Set();
|
|
93
|
+
constructor(config) {
|
|
94
|
+
this.issuer = config.issuer.replace(/\/+$/, "");
|
|
95
|
+
this.clientId = config.clientId;
|
|
96
|
+
this.storage = config.storage;
|
|
97
|
+
this.storageKey = config.storageKey;
|
|
98
|
+
this.fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
99
|
+
}
|
|
100
|
+
async hydrateFromStorage() {
|
|
101
|
+
const raw = await this.storage.get(this.storageKey);
|
|
102
|
+
if (!raw) return false;
|
|
103
|
+
try {
|
|
104
|
+
this.cache = JSON.parse(raw);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
await this.storage.delete(this.storageKey);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async persist(tokens) {
|
|
112
|
+
this.cache = tokens;
|
|
113
|
+
await this.storage.set(this.storageKey, JSON.stringify(tokens));
|
|
114
|
+
}
|
|
115
|
+
async getAccessToken() {
|
|
116
|
+
if (!this.cache) {
|
|
117
|
+
const ok = await this.hydrateFromStorage();
|
|
118
|
+
if (!ok) {
|
|
119
|
+
throw new OAuthError("invalid_grant", "No active session", 401);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!this.cache) {
|
|
123
|
+
throw new OAuthError("invalid_grant", "No active session", 401);
|
|
124
|
+
}
|
|
125
|
+
const remaining = this.cache.access_expires_at - Date.now();
|
|
126
|
+
if (remaining > PROACTIVE_WINDOW_MS) {
|
|
127
|
+
return this.cache.access_token;
|
|
128
|
+
}
|
|
129
|
+
return this.refresh();
|
|
130
|
+
}
|
|
131
|
+
/** Single-flight refresh — concurrent callers await the same promise. */
|
|
132
|
+
async refresh() {
|
|
133
|
+
if (this.inflightRefresh) return this.inflightRefresh;
|
|
134
|
+
if (!this.cache) {
|
|
135
|
+
throw new OAuthError("invalid_grant", "No refresh token available", 401);
|
|
136
|
+
}
|
|
137
|
+
const refreshToken = this.cache.refresh_token;
|
|
138
|
+
const previousScope = this.cache.scope;
|
|
139
|
+
this.inflightRefresh = (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const res = await this.fetch(`${this.issuer}/auth/token`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
grant_type: "refresh_token",
|
|
146
|
+
refresh_token: refreshToken,
|
|
147
|
+
client_id: this.clientId
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
const body = await res.json();
|
|
151
|
+
if (!res.ok || !body.access_token) {
|
|
152
|
+
await this.signOut();
|
|
153
|
+
throw new OAuthError(
|
|
154
|
+
body.error ?? "invalid_grant",
|
|
155
|
+
body.error ?? "Refresh failed",
|
|
156
|
+
res.status
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const expiresInMs = (body.expires_in ?? 3600) * 1e3;
|
|
160
|
+
const updated = {
|
|
161
|
+
access_token: body.access_token,
|
|
162
|
+
refresh_token: body.refresh_token ?? refreshToken,
|
|
163
|
+
access_expires_at: Date.now() + expiresInMs,
|
|
164
|
+
scope: body.scope ?? previousScope
|
|
165
|
+
};
|
|
166
|
+
await this.persist(updated);
|
|
167
|
+
return updated.access_token;
|
|
168
|
+
} finally {
|
|
169
|
+
this.inflightRefresh = null;
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
172
|
+
return this.inflightRefresh;
|
|
173
|
+
}
|
|
174
|
+
onSignOut(handler) {
|
|
175
|
+
this.signOutHandlers.add(handler);
|
|
176
|
+
return () => this.signOutHandlers.delete(handler);
|
|
177
|
+
}
|
|
178
|
+
async signOut() {
|
|
179
|
+
this.cache = null;
|
|
180
|
+
await this.storage.delete(this.storageKey);
|
|
181
|
+
for (const h of this.signOutHandlers) {
|
|
182
|
+
try {
|
|
183
|
+
h();
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/auth/auth.ts
|
|
191
|
+
var MymeAuth = class {
|
|
192
|
+
issuer;
|
|
193
|
+
clientId;
|
|
194
|
+
redirectUri;
|
|
195
|
+
scopes;
|
|
196
|
+
storage;
|
|
197
|
+
fetch;
|
|
198
|
+
pendingKey;
|
|
199
|
+
tokensKey;
|
|
200
|
+
constructor(config) {
|
|
201
|
+
this.issuer = config.issuer.replace(/\/+$/, "");
|
|
202
|
+
this.clientId = config.clientId;
|
|
203
|
+
this.redirectUri = config.redirectUri;
|
|
204
|
+
this.scopes = config.scopes;
|
|
205
|
+
this.storage = config.storage ?? defaultTokenStorage();
|
|
206
|
+
this.fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
207
|
+
const origin = (() => {
|
|
208
|
+
try {
|
|
209
|
+
return new URL(this.issuer).origin;
|
|
210
|
+
} catch {
|
|
211
|
+
return this.issuer;
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
this.pendingKey = `myme.auth.pending:${origin}:${config.clientId}`;
|
|
215
|
+
this.tokensKey = `myme.auth.tokens:${origin}:${config.clientId}`;
|
|
216
|
+
}
|
|
217
|
+
/** Build the authorize URL and persist the PKCE verifier + state. */
|
|
218
|
+
async buildAuthorizeUrl(opts) {
|
|
219
|
+
const verifier = generateCodeVerifier();
|
|
220
|
+
const challenge = await computeCodeChallenge(verifier);
|
|
221
|
+
const state = opts?.state ?? generateState();
|
|
222
|
+
const pending = {
|
|
223
|
+
verifier,
|
|
224
|
+
state,
|
|
225
|
+
redirectUri: this.redirectUri
|
|
226
|
+
};
|
|
227
|
+
await this.storage.set(this.pendingKey, JSON.stringify(pending));
|
|
228
|
+
const url = new URL(`${this.issuer}/auth/authorize`);
|
|
229
|
+
url.searchParams.set("response_type", "code");
|
|
230
|
+
url.searchParams.set("client_id", this.clientId);
|
|
231
|
+
url.searchParams.set("redirect_uri", this.redirectUri);
|
|
232
|
+
url.searchParams.set("scope", this.scopes.join(" "));
|
|
233
|
+
url.searchParams.set("code_challenge", challenge);
|
|
234
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
235
|
+
url.searchParams.set("state", state);
|
|
236
|
+
return url.toString();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Exchange the authorization code for tokens. Call from your `/callback`
|
|
240
|
+
* route handler with `window.location.href` (or the equivalent server-
|
|
241
|
+
* side request URL). Throws OAuthError on failure.
|
|
242
|
+
*/
|
|
243
|
+
async handleCallback(callbackUrl) {
|
|
244
|
+
const url = new URL(callbackUrl);
|
|
245
|
+
const error = url.searchParams.get("error");
|
|
246
|
+
if (error) {
|
|
247
|
+
throw new OAuthError(
|
|
248
|
+
error === "access_denied" ? "access_denied" : "invalid_request",
|
|
249
|
+
url.searchParams.get("error_description") ?? error
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const code = url.searchParams.get("code");
|
|
253
|
+
const incomingState = url.searchParams.get("state");
|
|
254
|
+
if (!code) {
|
|
255
|
+
throw new OAuthError("invalid_request", "Missing authorization code");
|
|
256
|
+
}
|
|
257
|
+
const pendingRaw = await this.storage.get(this.pendingKey);
|
|
258
|
+
if (!pendingRaw) {
|
|
259
|
+
throw new OAuthError(
|
|
260
|
+
"invalid_request",
|
|
261
|
+
"No pending PKCE verifier \u2014 did you call buildAuthorizeUrl in a different browser context?"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const pending = JSON.parse(pendingRaw);
|
|
265
|
+
if (pending.state !== incomingState) {
|
|
266
|
+
throw new OAuthError("invalid_request", "State mismatch on callback");
|
|
267
|
+
}
|
|
268
|
+
const res = await this.fetch(`${this.issuer}/auth/token`, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "Content-Type": "application/json" },
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
grant_type: "authorization_code",
|
|
273
|
+
code,
|
|
274
|
+
code_verifier: pending.verifier,
|
|
275
|
+
redirect_uri: pending.redirectUri,
|
|
276
|
+
client_id: this.clientId
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
const body = await res.json();
|
|
280
|
+
if (!res.ok || !body.access_token || !body.refresh_token) {
|
|
281
|
+
const errCode = body.error ? body.error : "invalid_grant";
|
|
282
|
+
throw new OAuthError(
|
|
283
|
+
errCode,
|
|
284
|
+
body.error_description ?? body.error ?? "Token exchange failed",
|
|
285
|
+
res.status
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
await this.storage.delete(this.pendingKey);
|
|
289
|
+
const provider = new StoredTokenProvider({
|
|
290
|
+
issuer: this.issuer,
|
|
291
|
+
clientId: this.clientId,
|
|
292
|
+
storage: this.storage,
|
|
293
|
+
storageKey: this.tokensKey,
|
|
294
|
+
fetch: this.fetch
|
|
295
|
+
});
|
|
296
|
+
await provider.persist({
|
|
297
|
+
access_token: body.access_token,
|
|
298
|
+
refresh_token: body.refresh_token,
|
|
299
|
+
access_expires_at: Date.now() + (body.expires_in ?? 3600) * 1e3,
|
|
300
|
+
scope: body.scope ?? this.scopes.join(" ")
|
|
301
|
+
});
|
|
302
|
+
return provider;
|
|
303
|
+
}
|
|
304
|
+
/** Restore a TokenProvider from persisted storage; null if no session. */
|
|
305
|
+
async restore() {
|
|
306
|
+
const provider = new StoredTokenProvider({
|
|
307
|
+
issuer: this.issuer,
|
|
308
|
+
clientId: this.clientId,
|
|
309
|
+
storage: this.storage,
|
|
310
|
+
storageKey: this.tokensKey,
|
|
311
|
+
fetch: this.fetch
|
|
312
|
+
});
|
|
313
|
+
const ok = await provider.hydrateFromStorage();
|
|
314
|
+
return ok ? provider : null;
|
|
315
|
+
}
|
|
316
|
+
/** Sign out — clears persisted tokens. */
|
|
317
|
+
async signOut(provider) {
|
|
318
|
+
await provider.signOut();
|
|
319
|
+
await this.storage.delete(this.pendingKey);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
export {
|
|
323
|
+
InMemoryTokenStorage,
|
|
324
|
+
LocalStorageTokenStorage,
|
|
325
|
+
MymeAuth,
|
|
326
|
+
OAuthError,
|
|
327
|
+
computeCodeChallenge,
|
|
328
|
+
defaultTokenStorage,
|
|
329
|
+
generateCodeVerifier,
|
|
330
|
+
generateState
|
|
331
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -47,9 +47,25 @@ interface ConflictAutoMergedEvent {
|
|
|
47
47
|
/** Optional callback fired when the auto-merge path completes successfully. */
|
|
48
48
|
type ConflictAutoMergeListener = (event: ConflictAutoMergedEvent) => void | Promise<void>;
|
|
49
49
|
|
|
50
|
-
interface
|
|
51
|
-
|
|
50
|
+
/** Minimal token provider shape — full interface lives in
|
|
51
|
+
* @mymehq/sdk/auth. Kept loose here so the data root doesn't depend
|
|
52
|
+
* on the auth subpath. */
|
|
53
|
+
interface TokenProviderLike {
|
|
54
|
+
getAccessToken(): Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Credentials are mutually exclusive at the type level: pass either a
|
|
58
|
+
* static API key (myme_k1_*) or an OAuth token provider (myme_at_*).
|
|
59
|
+
*/
|
|
60
|
+
type ClientCredential = {
|
|
52
61
|
apiKey: string;
|
|
62
|
+
tokenProvider?: never;
|
|
63
|
+
} | {
|
|
64
|
+
tokenProvider: TokenProviderLike;
|
|
65
|
+
apiKey?: never;
|
|
66
|
+
};
|
|
67
|
+
type ClientConfig = ClientCredential & {
|
|
68
|
+
url: string;
|
|
53
69
|
fetch?: typeof globalThis.fetch;
|
|
54
70
|
/**
|
|
55
71
|
* Default conflict resolution strategy for all updates.
|
|
@@ -67,7 +83,7 @@ interface ClientConfig {
|
|
|
67
83
|
onConflictAutoMerge?: ConflictAutoMergeListener;
|
|
68
84
|
timeoutMs?: number;
|
|
69
85
|
cdnBaseUrl?: string;
|
|
70
|
-
}
|
|
86
|
+
};
|
|
71
87
|
interface UpdateOptions {
|
|
72
88
|
/**
|
|
73
89
|
* Version the caller expects the item to currently be at. When provided,
|
|
@@ -510,9 +526,9 @@ declare class MymeClient {
|
|
|
510
526
|
limit?: number;
|
|
511
527
|
}) => Promise<WebhookDelivery[]>;
|
|
512
528
|
};
|
|
513
|
-
/** Tenant-scoped configuration
|
|
514
|
-
*
|
|
515
|
-
* are admin-only. */
|
|
529
|
+
/** Tenant-scoped configuration. Controls feed-tier retention per type
|
|
530
|
+
* and the three optional schema-enforcement levers (TSC42 §5). All
|
|
531
|
+
* endpoints are admin-only. */
|
|
516
532
|
readonly tenants: {
|
|
517
533
|
/** Returns the current tenant's config. Empty object when nothing
|
|
518
534
|
* is configured. */
|
package/dist/index.js
CHANGED
|
@@ -55,14 +55,35 @@ var DEFAULT_TIMEOUT_MS = 3e4;
|
|
|
55
55
|
var HttpTransport = class {
|
|
56
56
|
baseUrl;
|
|
57
57
|
apiKey;
|
|
58
|
+
tokenProvider;
|
|
58
59
|
fetch;
|
|
59
60
|
timeoutMs;
|
|
60
61
|
constructor(config) {
|
|
61
62
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
62
63
|
this.apiKey = config.apiKey;
|
|
64
|
+
this.tokenProvider = config.tokenProvider;
|
|
65
|
+
if (!this.apiKey && !this.tokenProvider) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"MymeClient requires either { apiKey } or { tokenProvider }"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
63
70
|
this.fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
64
71
|
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
65
72
|
}
|
|
73
|
+
/** Resolves the current Authorization header value. For tokenProvider
|
|
74
|
+
* callers this may trigger a proactive refresh under the hood. */
|
|
75
|
+
async getAuthHeader() {
|
|
76
|
+
if (this.apiKey) return `Bearer ${this.apiKey}`;
|
|
77
|
+
if (!this.tokenProvider) {
|
|
78
|
+
throw new MymeError(
|
|
79
|
+
"configuration_error",
|
|
80
|
+
"MymeClient has no apiKey or tokenProvider \u2014 this should be unreachable",
|
|
81
|
+
0
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const token = await this.tokenProvider.getAccessToken();
|
|
85
|
+
return `Bearer ${token}`;
|
|
86
|
+
}
|
|
66
87
|
async request(method, path, options) {
|
|
67
88
|
const response = await this.rawRequest(method, path, options);
|
|
68
89
|
if (response.status === 204) {
|
|
@@ -93,7 +114,7 @@ var HttpTransport = class {
|
|
|
93
114
|
async rawRequest(method, path, options) {
|
|
94
115
|
const url = this.buildUrl(path, options?.query);
|
|
95
116
|
const headers = {
|
|
96
|
-
Authorization:
|
|
117
|
+
Authorization: await this.getAuthHeader(),
|
|
97
118
|
...options?.headers
|
|
98
119
|
};
|
|
99
120
|
const controller = new AbortController();
|
|
@@ -320,6 +341,7 @@ var MymeClient = class {
|
|
|
320
341
|
this.transport = new HttpTransport({
|
|
321
342
|
baseUrl: config.url,
|
|
322
343
|
apiKey: config.apiKey,
|
|
344
|
+
tokenProvider: config.tokenProvider,
|
|
323
345
|
fetch: config.fetch,
|
|
324
346
|
timeoutMs: config.timeoutMs
|
|
325
347
|
});
|
|
@@ -814,9 +836,9 @@ var MymeClient = class {
|
|
|
814
836
|
}
|
|
815
837
|
};
|
|
816
838
|
// ---- Tenants (admin) ----
|
|
817
|
-
/** Tenant-scoped configuration
|
|
818
|
-
*
|
|
819
|
-
* are admin-only. */
|
|
839
|
+
/** Tenant-scoped configuration. Controls feed-tier retention per type
|
|
840
|
+
* and the three optional schema-enforcement levers (TSC42 §5). All
|
|
841
|
+
* endpoints are admin-only. */
|
|
820
842
|
tenants = {
|
|
821
843
|
/** Returns the current tenant's config. Empty object when nothing
|
|
822
844
|
* is configured. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mymehq/sdk",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./auth": {
|
|
15
|
+
"types": "./dist/auth/index.d.ts",
|
|
16
|
+
"import": "./dist/auth/index.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
16
20
|
"dist"
|
|
17
21
|
],
|
|
18
22
|
"dependencies": {
|
|
19
|
-
"@mymehq/shared": "4.
|
|
23
|
+
"@mymehq/shared": "4.1.1"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
26
|
"@types/node": "^22.0.0",
|