@marianmeres/ownsuite 1.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @module adapters/mock-auth
3
+ *
4
+ * In-memory mock implementations of AuthAdapter and ProfileAdapter.
5
+ * Drives the unit tests for AuthManager / ProfileManager / SessionManager
6
+ * without a real server. Consumers can also use it to build demos or
7
+ * storybooks that exercise the full suite.
8
+ *
9
+ * Deliberately small: everything is held in a shared `Store` object the
10
+ * adapters close over, so tests can peek at or mutate state directly.
11
+ */
12
+ import type { AuthAdapter, OAuthConnection, ProfileAdapter } from "../types/mod.js";
13
+ interface MockAccount {
14
+ email: string;
15
+ password: string;
16
+ roles: string[];
17
+ isVerified: boolean;
18
+ hasPassword: boolean;
19
+ oauthConnections: OAuthConnection[];
20
+ }
21
+ export interface MockAuthStore {
22
+ accounts: Map<string, MockAccount>;
23
+ /** If true, register/login return requiresVerification=true until the
24
+ * email is explicitly verified via `verifyMockAccount()`. */
25
+ requireVerifiedEmail: boolean;
26
+ /** Last-issued JWT per email (just a synthetic string). */
27
+ jwtsByEmail: Map<string, string>;
28
+ }
29
+ export declare function createMockAuthStore(init?: {
30
+ requireVerifiedEmail?: boolean;
31
+ seed?: MockAccount[];
32
+ }): MockAuthStore;
33
+ export declare function createMockAuthAdapter(store: MockAuthStore): AuthAdapter;
34
+ export declare function createMockProfileAdapter(store: MockAuthStore): ProfileAdapter;
35
+ /** Test helper — mark a mock account as verified as if the user had clicked
36
+ * the link in the verification email. */
37
+ export declare function verifyMockAccount(store: MockAuthStore, email: string): void;
38
+ export {};
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @module adapters/mock-auth
3
+ *
4
+ * In-memory mock implementations of AuthAdapter and ProfileAdapter.
5
+ * Drives the unit tests for AuthManager / ProfileManager / SessionManager
6
+ * without a real server. Consumers can also use it to build demos or
7
+ * storybooks that exercise the full suite.
8
+ *
9
+ * Deliberately small: everything is held in a shared `Store` object the
10
+ * adapters close over, so tests can peek at or mutate state directly.
11
+ */
12
+ export function createMockAuthStore(init = {}) {
13
+ const store = {
14
+ accounts: new Map(),
15
+ requireVerifiedEmail: init.requireVerifiedEmail ?? false,
16
+ jwtsByEmail: new Map(),
17
+ };
18
+ for (const a of init.seed ?? []) {
19
+ store.accounts.set(a.email, { ...a });
20
+ }
21
+ return store;
22
+ }
23
+ function mintJwt(email) {
24
+ return `mock.${btoa(email)}.${Date.now().toString(36)}`;
25
+ }
26
+ export function createMockAuthAdapter(store) {
27
+ return {
28
+ register(input, _ctx) {
29
+ if (store.accounts.has(input.email)) {
30
+ return Promise.reject(Object.assign(new Error("Conflict"), { status: 409 }));
31
+ }
32
+ const roles = (input.roles ?? ["user"]).filter((r) => !["admin", "root", "guest"].includes(r));
33
+ const effectiveRoles = roles.length > 0 ? roles : ["user"];
34
+ const account = {
35
+ email: input.email,
36
+ password: input.password,
37
+ roles: effectiveRoles,
38
+ isVerified: false,
39
+ hasPassword: true,
40
+ oauthConnections: [],
41
+ };
42
+ store.accounts.set(input.email, account);
43
+ if (store.requireVerifiedEmail) {
44
+ return Promise.resolve({
45
+ email: input.email,
46
+ roles: effectiveRoles,
47
+ requiresVerification: true,
48
+ });
49
+ }
50
+ const jwt = mintJwt(input.email);
51
+ store.jwtsByEmail.set(input.email, jwt);
52
+ return Promise.resolve({
53
+ jwt,
54
+ email: input.email,
55
+ roles: effectiveRoles,
56
+ isVerified: account.isVerified,
57
+ });
58
+ },
59
+ login(input, _ctx) {
60
+ const acc = store.accounts.get(input.email);
61
+ if (!acc || acc.password !== input.password) {
62
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
63
+ }
64
+ if (store.requireVerifiedEmail && !acc.isVerified) {
65
+ return Promise.reject(Object.assign(new Error("EMAIL_NOT_VERIFIED"), {
66
+ status: 403,
67
+ code: "EMAIL_NOT_VERIFIED",
68
+ }));
69
+ }
70
+ const jwt = mintJwt(input.email);
71
+ store.jwtsByEmail.set(input.email, jwt);
72
+ return Promise.resolve({
73
+ jwt,
74
+ email: acc.email,
75
+ roles: acc.roles,
76
+ isVerified: acc.isVerified,
77
+ });
78
+ },
79
+ logout(ctx) {
80
+ // Revoke the JWT associated with this bearer.
81
+ const jwt = ctx.jwt;
82
+ if (jwt) {
83
+ for (const [email, j] of store.jwtsByEmail) {
84
+ if (j === jwt) {
85
+ store.jwtsByEmail.delete(email);
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ return Promise.resolve();
91
+ },
92
+ oauthInitUrl(provider, opts) {
93
+ const qs = new URLSearchParams({ action: opts.action });
94
+ if (opts.redirect)
95
+ qs.set("redirect", opts.redirect);
96
+ if (opts.lang)
97
+ qs.set("lang", opts.lang);
98
+ return `mock://oauth/${provider}/init?${qs}`;
99
+ },
100
+ resendVerification(input, _ctx) {
101
+ // Silently succeed whether or not the account exists.
102
+ if (!store.accounts.has(input.email))
103
+ return Promise.resolve();
104
+ // No-op in the mock — verifyMockAccount() stands in for the user
105
+ // clicking the link.
106
+ return Promise.resolve();
107
+ },
108
+ requestPasswordReset(_input, _ctx) {
109
+ return Promise.resolve();
110
+ },
111
+ changePassword(input, ctx) {
112
+ const jwt = ctx.jwt;
113
+ let email;
114
+ if (jwt) {
115
+ for (const [e, j] of store.jwtsByEmail) {
116
+ if (j === jwt) {
117
+ email = e;
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ if (!email) {
123
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
124
+ }
125
+ const acc = store.accounts.get(email);
126
+ if (!acc) {
127
+ return Promise.reject(Object.assign(new Error("NotFound"), { status: 404 }));
128
+ }
129
+ if (acc.password !== input.current_password) {
130
+ return Promise.reject(Object.assign(new Error("BadRequest"), { status: 400 }));
131
+ }
132
+ acc.password = input.new_password;
133
+ return Promise.resolve();
134
+ },
135
+ deleteAccount(_input, ctx) {
136
+ const jwt = ctx.jwt;
137
+ if (!jwt) {
138
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
139
+ }
140
+ for (const [email, j] of store.jwtsByEmail) {
141
+ if (j === jwt) {
142
+ store.accounts.delete(email);
143
+ store.jwtsByEmail.delete(email);
144
+ return Promise.resolve({ deleted: true });
145
+ }
146
+ }
147
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
148
+ },
149
+ };
150
+ }
151
+ export function createMockProfileAdapter(store) {
152
+ function emailFromCtx(ctx) {
153
+ const jwt = ctx.jwt;
154
+ if (!jwt)
155
+ return null;
156
+ for (const [email, j] of store.jwtsByEmail) {
157
+ if (j === jwt)
158
+ return email;
159
+ }
160
+ return null;
161
+ }
162
+ return {
163
+ get(ctx) {
164
+ const email = emailFromCtx(ctx);
165
+ if (!email) {
166
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
167
+ }
168
+ const acc = store.accounts.get(email);
169
+ if (!acc) {
170
+ return Promise.reject(Object.assign(new Error("NotFound"), { status: 404 }));
171
+ }
172
+ return Promise.resolve({
173
+ email: acc.email,
174
+ roles: acc.roles,
175
+ isVerified: acc.isVerified,
176
+ hasPassword: acc.hasPassword,
177
+ oauthConnections: [...acc.oauthConnections],
178
+ });
179
+ },
180
+ update(input, ctx) {
181
+ const email = emailFromCtx(ctx);
182
+ if (!email) {
183
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
184
+ }
185
+ const acc = store.accounts.get(email);
186
+ if (!acc) {
187
+ return Promise.reject(Object.assign(new Error("NotFound"), { status: 404 }));
188
+ }
189
+ if (input.email && input.email !== acc.email) {
190
+ if (store.requireVerifiedEmail)
191
+ acc.isVerified = false;
192
+ store.accounts.delete(acc.email);
193
+ acc.email = input.email;
194
+ store.accounts.set(acc.email, acc);
195
+ // Refresh JWT mapping too.
196
+ const jwt = store.jwtsByEmail.get(email);
197
+ if (jwt) {
198
+ store.jwtsByEmail.delete(email);
199
+ store.jwtsByEmail.set(acc.email, jwt);
200
+ }
201
+ }
202
+ return Promise.resolve({
203
+ email: acc.email,
204
+ roles: acc.roles,
205
+ isVerified: acc.isVerified,
206
+ hasPassword: acc.hasPassword,
207
+ oauthConnections: [...acc.oauthConnections],
208
+ });
209
+ },
210
+ listOAuth(ctx) {
211
+ const email = emailFromCtx(ctx);
212
+ if (!email)
213
+ return Promise.resolve([]);
214
+ const acc = store.accounts.get(email);
215
+ return Promise.resolve(acc ? [...acc.oauthConnections] : []);
216
+ },
217
+ unlinkOAuth(provider, ctx) {
218
+ const email = emailFromCtx(ctx);
219
+ if (!email) {
220
+ return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 }));
221
+ }
222
+ const acc = store.accounts.get(email);
223
+ if (!acc) {
224
+ return Promise.reject(Object.assign(new Error("NotFound"), { status: 404 }));
225
+ }
226
+ acc.oauthConnections = acc.oauthConnections.filter((c) => c.provider !== provider);
227
+ return Promise.resolve();
228
+ },
229
+ };
230
+ }
231
+ /** Test helper — mark a mock account as verified as if the user had clicked
232
+ * the link in the verification email. */
233
+ export function verifyMockAccount(store, email) {
234
+ const acc = store.accounts.get(email);
235
+ if (acc)
236
+ acc.isVerified = true;
237
+ }
@@ -2,9 +2,10 @@
2
2
  * @module adapters/mock
3
3
  *
4
4
  * In-memory mock adapter for testing. Stores rows in a local Map keyed by
5
- * `model_id`, applies an optional latency, and can inject failures. Useful
6
- * for unit tests without a real server, and for exercising the manager's
7
- * optimistic-update rollback path deterministically.
5
+ * `model_id`, applies an optional latency, honors `ctx.signal` for
6
+ * cancellation, and can inject failures. Useful for unit tests without a
7
+ * real server, and for exercising the manager's optimistic-update rollback
8
+ * path deterministically.
8
9
  */
9
10
  import type { OwnedCollectionAdapter } from "../types/adapter.js";
10
11
  export interface MockAdapterOptions<TRow> {
@@ -24,6 +25,12 @@ export interface MockAdapterOptions<TRow> {
24
25
  getRowId?: (row: TRow) => string;
25
26
  /** Factory for new row ids (defaults to `crypto.randomUUID`). */
26
27
  newId?: () => string;
28
+ /**
29
+ * If true (default), `create` rejects payloads that include a
30
+ * `model_id` — matches the production server contract where the server
31
+ * is authoritative over ids. Set to `false` to bypass for legacy tests.
32
+ */
33
+ rejectClientId?: boolean;
27
34
  }
28
35
  /**
29
36
  * Build an in-memory `OwnedCollectionAdapter` for tests.
@@ -2,68 +2,122 @@
2
2
  * @module adapters/mock
3
3
  *
4
4
  * In-memory mock adapter for testing. Stores rows in a local Map keyed by
5
- * `model_id`, applies an optional latency, and can inject failures. Useful
6
- * for unit tests without a real server, and for exercising the manager's
7
- * optimistic-update rollback path deterministically.
5
+ * `model_id`, applies an optional latency, honors `ctx.signal` for
6
+ * cancellation, and can inject failures. Useful for unit tests without a
7
+ * real server, and for exercising the manager's optimistic-update rollback
8
+ * path deterministically.
8
9
  */
9
10
  const defaultGetRowId = (r) => {
10
11
  const rec = r;
11
12
  const id = rec.model_id ?? rec.id;
12
- if (typeof id !== "string") {
13
- throw new Error("MockAdapter: row has no string `model_id` or `id`; pass `getRowId`");
13
+ if (typeof id !== "string" || id === "") {
14
+ throw new Error("MockAdapter: row has no non-empty string `model_id` or `id`; pass `getRowId`");
14
15
  }
15
16
  return id;
16
17
  };
18
+ function safeClone(value) {
19
+ if (value === null || value === undefined)
20
+ return value;
21
+ try {
22
+ return structuredClone(value);
23
+ }
24
+ catch {
25
+ try {
26
+ return JSON.parse(JSON.stringify(value));
27
+ }
28
+ catch {
29
+ return value;
30
+ }
31
+ }
32
+ }
33
+ /** Throw an AbortError-shaped error if the signal was aborted. */
34
+ function throwIfAborted(signal) {
35
+ if (signal?.aborted) {
36
+ const reason = signal.reason;
37
+ const err = new Error(typeof reason === "string" ? `mock: aborted (${reason})` : "mock: aborted");
38
+ err.name = "AbortError";
39
+ throw err;
40
+ }
41
+ }
17
42
  /**
18
43
  * Build an in-memory `OwnedCollectionAdapter` for tests.
19
44
  */
20
45
  export function createMockOwnedCollectionAdapter(options = {}) {
21
- const { delayMs = 0, failOn = {}, getRowId = defaultGetRowId, newId = () => crypto.randomUUID(), } = options;
46
+ const { delayMs = 0, failOn = {}, getRowId = defaultGetRowId, newId = () => crypto.randomUUID(), rejectClientId = true, } = options;
22
47
  const store = new Map();
23
48
  for (const r of options.seed ?? [])
24
49
  store.set(getRowId(r), r);
25
- const sleep = () => delayMs > 0
26
- ? new Promise((res) => setTimeout(res, delayMs))
27
- : Promise.resolve();
50
+ /**
51
+ * Latency helper that also observes abort. Returns when either the
52
+ * delay elapses or the signal fires — the caller then checks
53
+ * `throwIfAborted` to convert to an error.
54
+ */
55
+ const sleep = (signal) => {
56
+ if (delayMs <= 0)
57
+ return Promise.resolve();
58
+ return new Promise((resolve) => {
59
+ const t = setTimeout(resolve, delayMs);
60
+ signal?.addEventListener("abort", () => {
61
+ clearTimeout(t);
62
+ resolve();
63
+ }, { once: true });
64
+ });
65
+ };
28
66
  return {
29
- async list(_ctx, _query) {
30
- await sleep();
67
+ async list(ctx, _query) {
68
+ await sleep(ctx.signal);
69
+ throwIfAborted(ctx.signal);
31
70
  if (failOn.list)
32
71
  throw new Error("mock: list failed");
33
- const rows = [...store.values()];
72
+ const rows = [...store.values()].map((r) => safeClone(r));
34
73
  return { data: rows, meta: { total: rows.length } };
35
74
  },
36
- async getOne(id, _ctx) {
37
- await sleep();
75
+ async getOne(id, ctx) {
76
+ await sleep(ctx.signal);
77
+ throwIfAborted(ctx.signal);
38
78
  if (failOn.getOne)
39
79
  throw new Error("mock: getOne failed");
40
80
  const row = store.get(id);
41
81
  if (!row)
42
82
  throw new Error(`mock: row ${id} not found`);
43
- return { data: row };
83
+ return { data: safeClone(row) };
44
84
  },
45
- async create(data, _ctx) {
46
- await sleep();
85
+ async create(data, ctx) {
86
+ await sleep(ctx.signal);
87
+ throwIfAborted(ctx.signal);
47
88
  if (failOn.create)
48
89
  throw new Error("mock: create failed");
90
+ const input = data;
91
+ if (rejectClientId &&
92
+ input !== null &&
93
+ typeof input === "object" &&
94
+ "model_id" in input) {
95
+ throw new Error("mock: create payload must not include `model_id` — the server assigns the id");
96
+ }
49
97
  const id = newId();
50
- const row = { ...data, model_id: id };
98
+ const cloned = safeClone(data);
99
+ const row = { ...cloned, model_id: id };
51
100
  store.set(id, row);
52
- return { data: row };
101
+ return { data: safeClone(row) };
53
102
  },
54
- async update(id, data, _ctx) {
55
- await sleep();
103
+ async update(id, data, ctx) {
104
+ await sleep(ctx.signal);
105
+ throwIfAborted(ctx.signal);
56
106
  if (failOn.update)
57
107
  throw new Error("mock: update failed");
58
108
  const existing = store.get(id);
59
109
  if (!existing)
60
110
  throw new Error(`mock: row ${id} not found`);
61
- const merged = { ...existing, ...data };
111
+ const merged = {
112
+ ...existing,
113
+ ...safeClone(data),
114
+ };
62
115
  store.set(id, merged);
63
- return { data: merged };
116
+ return { data: safeClone(merged) };
64
117
  },
65
- async delete(id, _ctx) {
66
- await sleep();
118
+ async delete(id, ctx) {
119
+ await sleep(ctx.signal);
120
+ throwIfAborted(ctx.signal);
67
121
  if (failOn.delete)
68
122
  throw new Error("mock: delete failed");
69
123
  return store.delete(id);
@@ -1 +1,3 @@
1
1
  export * from "./mock.js";
2
+ export * from "./mock-auth.js";
3
+ export * from "./stack-account.js";
@@ -1 +1,3 @@
1
1
  export * from "./mock.js";
2
+ export * from "./mock-auth.js";
3
+ export * from "./stack-account.js";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @module adapters/stack-account
3
+ *
4
+ * Default implementations of `AuthAdapter` and `ProfileAdapter` that target
5
+ * the `@marianmeres/stack-account` REST surface:
6
+ *
7
+ * POST /api/account/register
8
+ * POST /api/account/login
9
+ * POST /api/account/logout
10
+ * GET /api/account/oauth/[provider]/init
11
+ * POST /api/account/verify/resend
12
+ * POST /api/account/password/reset
13
+ * POST /api/account/password/change
14
+ * GET/PUT/DELETE /api/account/me
15
+ * GET /api/account/me/oauth
16
+ * DELETE /api/account/me/oauth/[provider]
17
+ *
18
+ * Apps whose server mounts stack-account at this path can just do:
19
+ *
20
+ * const suite = createOwnsuite({
21
+ * adapters: {
22
+ * auth: createStackAccountAuthAdapter({ baseUrl: "/api/account" }),
23
+ * profile: createStackAccountProfileAdapter({ baseUrl: "/api/account" }),
24
+ * },
25
+ * });
26
+ *
27
+ * Apps with custom routes should write their own adapter against the
28
+ * AuthAdapter / ProfileAdapter interfaces.
29
+ */
30
+ import type { AuthAdapter, ProfileAdapter } from "../types/mod.js";
31
+ export interface StackAccountAdapterOptions {
32
+ /** Base URL of the mounted stack-account app. Default: "/api/account". */
33
+ baseUrl?: string;
34
+ /** Override the `fetch` implementation (useful for tests / SSR). */
35
+ fetch?: typeof fetch;
36
+ }
37
+ export declare function createStackAccountAuthAdapter(opts?: StackAccountAdapterOptions): AuthAdapter;
38
+ export declare function createStackAccountProfileAdapter(opts?: StackAccountAdapterOptions): ProfileAdapter;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @module adapters/stack-account
3
+ *
4
+ * Default implementations of `AuthAdapter` and `ProfileAdapter` that target
5
+ * the `@marianmeres/stack-account` REST surface:
6
+ *
7
+ * POST /api/account/register
8
+ * POST /api/account/login
9
+ * POST /api/account/logout
10
+ * GET /api/account/oauth/[provider]/init
11
+ * POST /api/account/verify/resend
12
+ * POST /api/account/password/reset
13
+ * POST /api/account/password/change
14
+ * GET/PUT/DELETE /api/account/me
15
+ * GET /api/account/me/oauth
16
+ * DELETE /api/account/me/oauth/[provider]
17
+ *
18
+ * Apps whose server mounts stack-account at this path can just do:
19
+ *
20
+ * const suite = createOwnsuite({
21
+ * adapters: {
22
+ * auth: createStackAccountAuthAdapter({ baseUrl: "/api/account" }),
23
+ * profile: createStackAccountProfileAdapter({ baseUrl: "/api/account" }),
24
+ * },
25
+ * });
26
+ *
27
+ * Apps with custom routes should write their own adapter against the
28
+ * AuthAdapter / ProfileAdapter interfaces.
29
+ */
30
+ function resolveFetch(opts) {
31
+ return opts?.fetch ?? (globalThis.fetch.bind(globalThis));
32
+ }
33
+ function join(base, path) {
34
+ if (!base)
35
+ return path;
36
+ if (base.endsWith("/"))
37
+ return `${base.slice(0, -1)}${path}`;
38
+ return `${base}${path}`;
39
+ }
40
+ function authHeaders(ctx) {
41
+ return ctx.jwt ? { Authorization: `Bearer ${ctx.jwt}` } : {};
42
+ }
43
+ async function postJson(doFetch, url, body, ctx) {
44
+ const res = await doFetch(url, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ ...authHeaders(ctx),
49
+ },
50
+ body: JSON.stringify(body),
51
+ signal: ctx.signal,
52
+ });
53
+ if (!res.ok) {
54
+ const text = await res.text();
55
+ throw Object.assign(new Error(text || res.statusText), {
56
+ status: res.status,
57
+ body: text,
58
+ });
59
+ }
60
+ return (await res.json());
61
+ }
62
+ async function requestJson(doFetch, url, init, ctx) {
63
+ const res = await doFetch(url, {
64
+ ...init,
65
+ headers: {
66
+ ...(init.headers ?? {}),
67
+ ...authHeaders(ctx),
68
+ ...(init.body
69
+ ? { "Content-Type": "application/json" }
70
+ : {}),
71
+ },
72
+ signal: ctx.signal,
73
+ });
74
+ if (!res.ok) {
75
+ const text = await res.text();
76
+ throw Object.assign(new Error(text || res.statusText), {
77
+ status: res.status,
78
+ body: text,
79
+ });
80
+ }
81
+ if (res.status === 204)
82
+ return undefined;
83
+ return (await res.json());
84
+ }
85
+ export function createStackAccountAuthAdapter(opts = {}) {
86
+ const base = opts.baseUrl ?? "/api/account";
87
+ const doFetch = resolveFetch(opts);
88
+ return {
89
+ async register(input, ctx) {
90
+ const r = await postJson(doFetch, join(base, "/register"), input, ctx);
91
+ return r.data;
92
+ },
93
+ async login(input, ctx) {
94
+ const r = await postJson(doFetch, join(base, "/login"), input, ctx);
95
+ return r.data;
96
+ },
97
+ async logout(ctx) {
98
+ await requestJson(doFetch, join(base, "/logout"), { method: "POST" }, ctx);
99
+ },
100
+ oauthInitUrl(provider, options, _ctx) {
101
+ const qs = new URLSearchParams({ action: options.action });
102
+ if (options.redirect)
103
+ qs.set("redirect", options.redirect);
104
+ if (options.lang)
105
+ qs.set("lang", options.lang);
106
+ return `${join(base, `/oauth/${provider}/init`)}?${qs}`;
107
+ },
108
+ async resendVerification(input, ctx) {
109
+ await postJson(doFetch, join(base, "/verify/resend"), input, ctx);
110
+ },
111
+ async requestPasswordReset(input, ctx) {
112
+ await postJson(doFetch, join(base, "/password/reset"), input, ctx);
113
+ },
114
+ async changePassword(input, ctx) {
115
+ await postJson(doFetch, join(base, "/password/change"), input, ctx);
116
+ },
117
+ async deleteAccount(input, ctx) {
118
+ await requestJson(doFetch, join(base, "/me"), {
119
+ method: "DELETE",
120
+ body: JSON.stringify(input),
121
+ }, ctx);
122
+ return { deleted: true };
123
+ },
124
+ };
125
+ }
126
+ export function createStackAccountProfileAdapter(opts = {}) {
127
+ const base = opts.baseUrl ?? "/api/account";
128
+ const doFetch = resolveFetch(opts);
129
+ return {
130
+ async get(ctx) {
131
+ const r = await requestJson(doFetch, join(base, "/me"), { method: "GET" }, ctx);
132
+ return r.data;
133
+ },
134
+ async update(input, ctx) {
135
+ const r = await requestJson(doFetch, join(base, "/me"), {
136
+ method: "PUT",
137
+ body: JSON.stringify(input),
138
+ }, ctx);
139
+ return r.data;
140
+ },
141
+ async listOAuth(ctx) {
142
+ const r = await requestJson(doFetch, join(base, "/me/oauth"), { method: "GET" }, ctx);
143
+ return r.data ?? [];
144
+ },
145
+ async unlinkOAuth(provider, ctx) {
146
+ await requestJson(doFetch, join(base, `/me/oauth/${provider}`), { method: "DELETE" }, ctx);
147
+ },
148
+ };
149
+ }