@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.
@@ -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 ClientConfig {
51
- url: string;
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 / ambient state. Independent of the version-merge path
98
- * for `properties`; a library-only update never conflicts. */
99
- library?: boolean;
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
- /** Tri-value library filter. `true` restricts to library items; `false`
119
- * restricts to ambient items; omitting the field returns both (the V0
120
- * default). */
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
- /** Tri-value library filter, matching `ListFilters.library`. */
156
- library?: boolean;
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
- library?: boolean;
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
- library?: boolean;
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: "update_library";
299
- library: boolean;
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, default_library, type_permissions, and
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 (per-type ambient retention overrides
515
- * today; future tenant-level settings will live here). All endpoints
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: `Bearer ${this.apiKey}`,
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, library, onAutoMerge) {
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
- ...library !== void 0 && { library }
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?.library,
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, default_library, type_permissions, and
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 (per-type ambient retention overrides
818
- * today; future tenant-level settings will live here). All endpoints
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.8.0",
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": "3.5.0"
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.1"
29
+ "@mymehq/server": "1.0.0"
26
30
  },
27
31
  "scripts": {
28
32
  "build": "tsup",