@mymehq/sdk 3.8.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 +71 -22
- package/dist/index.js +36 -8
- package/package.json +7 -3
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MergeStrategy, ConflictSnapshot, MergePolicy, ItemState, Item, CreateItemInput, PaginatedResult, ItemWithMetadata, Version, Edge, Metadata, SearchResult, CreateEdgeInput, EdgeTypeSchema, TypeSchema, CreateKeyInput, ApiKey, UpdateKeyInput, CreateWebhookInput, Webhook, UpdateWebhookInput, WebhookDelivery, TenantConfig } from '@mymehq/shared';
|
|
1
|
+
import { MergeStrategy, ConflictSnapshot, MergePolicy, ItemState, Tier, Item, CreateItemInput, PaginatedResult, ItemWithMetadata, Version, Edge, Metadata, SearchResult, CreateEdgeInput, EdgeTypeSchema, TypeSchema, CreateKeyInput, ApiKey, UpdateKeyInput, CreateWebhookInput, Webhook, UpdateWebhookInput, WebhookDelivery, TenantConfig } from '@mymehq/shared';
|
|
2
2
|
export { ApiKey, ConflictSnapshot, CreateItemInput, CreateKeyInput, Item, ItemState, MergePolicy, MergeStrategy, Metadata, PaginatedResult, SearchResult, TypeSchema, UpdateKeyInput, Version } from '@mymehq/shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -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,
|
|
@@ -94,9 +110,9 @@ interface UpdateOptions {
|
|
|
94
110
|
conflict?: ConflictStrategy;
|
|
95
111
|
/** Custom conflict resolver (required when `conflict` is `"callback"`). */
|
|
96
112
|
resolve?: ConflictResolver;
|
|
97
|
-
/** Toggle library
|
|
98
|
-
* for `properties`; a
|
|
99
|
-
|
|
113
|
+
/** Toggle the tier (`library` ↔ `feed`). Independent of the version-merge
|
|
114
|
+
* path for `properties`; a tier-only update never conflicts. */
|
|
115
|
+
tier?: Tier;
|
|
100
116
|
/**
|
|
101
117
|
* Item type. Required by the `auto` strategy when a `keep_both_copies`
|
|
102
118
|
* conflict spawns a sibling item. Omit to let the SDK pre-fetch it —
|
|
@@ -115,10 +131,9 @@ interface ListFilters {
|
|
|
115
131
|
type?: string;
|
|
116
132
|
state?: ItemState;
|
|
117
133
|
source?: string;
|
|
118
|
-
/**
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
library?: boolean;
|
|
134
|
+
/** Tier filter. `"library"` restricts to library items; `"feed"` restricts
|
|
135
|
+
* to feed items; `"all"` (or omitting the field) returns both. */
|
|
136
|
+
tier?: Tier | "all";
|
|
122
137
|
tags?: string[];
|
|
123
138
|
/** Filter-language expression, e.g. `edge[parent-of] eq "<id>"` or
|
|
124
139
|
* `edge[parent-of] not_exists`. The filter language replaces the
|
|
@@ -152,8 +167,8 @@ type ItemWithExtensions = Item & {
|
|
|
152
167
|
interface SearchFilters {
|
|
153
168
|
type?: string;
|
|
154
169
|
state?: ItemState;
|
|
155
|
-
/**
|
|
156
|
-
|
|
170
|
+
/** Tier filter, matching `ListFilters.tier`. */
|
|
171
|
+
tier?: Tier | "all";
|
|
157
172
|
/** Items must have ALL specified tags (AND semantics). Matches
|
|
158
173
|
* `ListFilters.tags` and `/items?tags=`. */
|
|
159
174
|
tags?: string[];
|
|
@@ -170,7 +185,7 @@ interface BulkItemInput {
|
|
|
170
185
|
type: string;
|
|
171
186
|
properties?: Record<string, unknown>;
|
|
172
187
|
state?: ItemState;
|
|
173
|
-
|
|
188
|
+
tier?: Tier;
|
|
174
189
|
timestamp?: string;
|
|
175
190
|
/** Ignored on the wire — server stamps `source` from the credential.
|
|
176
191
|
* Kept on the input shape for round-trip parity with /export output. */
|
|
@@ -265,7 +280,7 @@ interface BulkActionFilter {
|
|
|
265
280
|
type?: string;
|
|
266
281
|
state?: ItemState;
|
|
267
282
|
source?: string;
|
|
268
|
-
|
|
283
|
+
tier?: Tier | "all";
|
|
269
284
|
tags?: string[];
|
|
270
285
|
since?: string;
|
|
271
286
|
until?: string;
|
|
@@ -295,8 +310,8 @@ type BulkActionInput = (BulkActionBase & {
|
|
|
295
310
|
add?: string[];
|
|
296
311
|
remove?: string[];
|
|
297
312
|
}) | (BulkActionBase & {
|
|
298
|
-
action: "
|
|
299
|
-
|
|
313
|
+
action: "update_tier";
|
|
314
|
+
tier: Tier;
|
|
300
315
|
}) | (BulkActionBase & {
|
|
301
316
|
action: "update_properties";
|
|
302
317
|
patch: Record<string, unknown>;
|
|
@@ -486,7 +501,7 @@ declare class MymeClient {
|
|
|
486
501
|
readonly keys: {
|
|
487
502
|
/** Creates an API key. The raw key value is returned exactly once on
|
|
488
503
|
* creation; the rest of the shape mirrors the persisted ApiKey record
|
|
489
|
-
* (source, default_origin,
|
|
504
|
+
* (source, default_origin, default_tier, type_permissions, and
|
|
490
505
|
* extension_permissions are all stamped at create time and visible
|
|
491
506
|
* here so the caller doesn't need a follow-up GET /keys to inspect
|
|
492
507
|
* them). */
|
|
@@ -511,9 +526,9 @@ declare class MymeClient {
|
|
|
511
526
|
limit?: number;
|
|
512
527
|
}) => Promise<WebhookDelivery[]>;
|
|
513
528
|
};
|
|
514
|
-
/** Tenant-scoped configuration
|
|
515
|
-
*
|
|
516
|
-
* 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. */
|
|
517
532
|
readonly tenants: {
|
|
518
533
|
/** Returns the current tenant's config. Empty object when nothing
|
|
519
534
|
* is configured. */
|
|
@@ -574,6 +589,40 @@ declare class ConflictError extends MymeError {
|
|
|
574
589
|
constructor(current: ConflictSnapshot, ancestor: ConflictSnapshot, conflictingFields: string[], clientPatch: Record<string, unknown>);
|
|
575
590
|
}
|
|
576
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Authoring helper for declaring custom Myme types in TypeScript (TSC42 §7).
|
|
594
|
+
*
|
|
595
|
+
* The function is a no-op at runtime — it returns its input unchanged. Its
|
|
596
|
+
* value is at the type level: `defineType` constrains the argument to a
|
|
597
|
+
* structurally-valid `TypeSchema`, surfacing field-shape mistakes at compile
|
|
598
|
+
* time rather than at `POST /types` rejection time. Pair with the SDK's
|
|
599
|
+
* `client.types.register(schema)` call to register the type with the server.
|
|
600
|
+
*
|
|
601
|
+
* Example:
|
|
602
|
+
*
|
|
603
|
+
* ```ts
|
|
604
|
+
* import { defineType } from "@mymehq/sdk";
|
|
605
|
+
*
|
|
606
|
+
* export const acmeDeal = defineType({
|
|
607
|
+
* id: "acme.deal",
|
|
608
|
+
* label: "Deal",
|
|
609
|
+
* description: "A sales pipeline opportunity",
|
|
610
|
+
* version: 1,
|
|
611
|
+
* fields: {
|
|
612
|
+
* name: { type: "string", description: "Deal name", required: true },
|
|
613
|
+
* amount: { type: "number", description: "Deal value (USD)" },
|
|
614
|
+
* },
|
|
615
|
+
* });
|
|
616
|
+
*
|
|
617
|
+
* await client.types.register(acmeDeal);
|
|
618
|
+
* ```
|
|
619
|
+
*
|
|
620
|
+
* The helper preserves the literal types of the schema (via the generic
|
|
621
|
+
* parameter), so downstream code that reads `acmeDeal.fields.name.type` sees
|
|
622
|
+
* the narrowed literal `"string"` rather than the wide `FieldType` union.
|
|
623
|
+
*/
|
|
624
|
+
declare function defineType<T extends TypeSchema>(schema: T): T;
|
|
625
|
+
|
|
577
626
|
/**
|
|
578
627
|
* Reason a webhook signature did not validate. Receivers should treat
|
|
579
628
|
* any non-`valid` result as "do not trust this delivery".
|
|
@@ -626,4 +675,4 @@ interface VerifyWebhookSignatureInput {
|
|
|
626
675
|
*/
|
|
627
676
|
declare function verifyWebhookSignature(input: VerifyWebhookSignatureInput): WebhookVerifyResult;
|
|
628
677
|
|
|
629
|
-
export { type BulkActionErrorEntry, type BulkActionFilter, type BulkActionInput, type BulkActionResult, type BulkEdgeInput, type BulkEdgeInputItem, type BulkEdgeResult, type BulkEdgeResultEntry, type BulkInput, type BulkItemInput, type BulkMode, type BulkOutcome, type BulkResult, type BulkResultEntry, type ClientConfig, type ConflictAutoMergeListener, type ConflictAutoMergedEvent, type ConflictData, ConflictError, type ConflictResolver, type ConflictStrategy, ForbiddenError, type ItemWithExtensions, type ListFilters, type MetadataInput, MymeClient, MymeError, NotFoundError, type SearchFilters, UnauthorizedError, type UpdateOptions, ValidationError, type VerifyWebhookSignatureInput, type WebhookVerifyReason, type WebhookVerifyResult, verifyWebhookSignature };
|
|
678
|
+
export { type BulkActionErrorEntry, type BulkActionFilter, type BulkActionInput, type BulkActionResult, type BulkEdgeInput, type BulkEdgeInputItem, type BulkEdgeResult, type BulkEdgeResultEntry, type BulkInput, type BulkItemInput, type BulkMode, type BulkOutcome, type BulkResult, type BulkResultEntry, type ClientConfig, type ConflictAutoMergeListener, type ConflictAutoMergedEvent, type ConflictData, ConflictError, type ConflictResolver, type ConflictStrategy, ForbiddenError, type ItemWithExtensions, type ListFilters, type MetadataInput, MymeClient, MymeError, NotFoundError, type SearchFilters, UnauthorizedError, type UpdateOptions, ValidationError, type VerifyWebhookSignatureInput, type WebhookVerifyReason, type WebhookVerifyResult, defineType, verifyWebhookSignature };
|
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();
|
|
@@ -239,7 +260,7 @@ function toConflictError(response, clientPatch) {
|
|
|
239
260
|
clientPatch
|
|
240
261
|
);
|
|
241
262
|
}
|
|
242
|
-
async function handleConflictUpdate(transport, itemId, itemType, clientPatch, version, strategy, resolver,
|
|
263
|
+
async function handleConflictUpdate(transport, itemId, itemType, clientPatch, version, strategy, resolver, tier, onAutoMerge) {
|
|
243
264
|
let properties = clientPatch;
|
|
244
265
|
let currentVersion = version;
|
|
245
266
|
let pendingAutoMergeEvent;
|
|
@@ -248,7 +269,7 @@ async function handleConflictUpdate(transport, itemId, itemType, clientPatch, ve
|
|
|
248
269
|
body: {
|
|
249
270
|
properties,
|
|
250
271
|
version: currentVersion,
|
|
251
|
-
...
|
|
272
|
+
...tier !== void 0 && { tier }
|
|
252
273
|
}
|
|
253
274
|
});
|
|
254
275
|
if (!isConflictResponse(result)) {
|
|
@@ -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
|
});
|
|
@@ -401,7 +423,7 @@ var MymeClient = class {
|
|
|
401
423
|
version,
|
|
402
424
|
strategy,
|
|
403
425
|
options?.resolve,
|
|
404
|
-
options?.
|
|
426
|
+
options?.tier,
|
|
405
427
|
onAutoMerge
|
|
406
428
|
);
|
|
407
429
|
},
|
|
@@ -750,7 +772,7 @@ var MymeClient = class {
|
|
|
750
772
|
keys = {
|
|
751
773
|
/** Creates an API key. The raw key value is returned exactly once on
|
|
752
774
|
* creation; the rest of the shape mirrors the persisted ApiKey record
|
|
753
|
-
* (source, default_origin,
|
|
775
|
+
* (source, default_origin, default_tier, type_permissions, and
|
|
754
776
|
* extension_permissions are all stamped at create time and visible
|
|
755
777
|
* here so the caller doesn't need a follow-up GET /keys to inspect
|
|
756
778
|
* them). */
|
|
@@ -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. */
|
|
@@ -862,6 +884,11 @@ var MymeClient = class {
|
|
|
862
884
|
}
|
|
863
885
|
};
|
|
864
886
|
|
|
887
|
+
// src/define-type.ts
|
|
888
|
+
function defineType(schema) {
|
|
889
|
+
return schema;
|
|
890
|
+
}
|
|
891
|
+
|
|
865
892
|
// src/webhooks.ts
|
|
866
893
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
867
894
|
var STRIPE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
@@ -900,5 +927,6 @@ export {
|
|
|
900
927
|
NotFoundError,
|
|
901
928
|
UnauthorizedError,
|
|
902
929
|
ValidationError,
|
|
930
|
+
defineType,
|
|
903
931
|
verifyWebhookSignature
|
|
904
932
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mymehq/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -10,19 +10,23 @@
|
|
|
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": "
|
|
23
|
+
"@mymehq/shared": "4.1.1"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
26
|
"@types/node": "^22.0.0",
|
|
23
27
|
"tsup": "^8.5.1",
|
|
24
28
|
"typescript": "^6.0.2",
|
|
25
|
-
"@mymehq/server": "0.0
|
|
29
|
+
"@mymehq/server": "1.0.0"
|
|
26
30
|
},
|
|
27
31
|
"scripts": {
|
|
28
32
|
"build": "tsup",
|