naro-js 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -49,6 +49,12 @@ function invalidProvider() {
49
49
  message: "provider must be google or github."
50
50
  };
51
51
  }
52
+ function serviceKeyRequired() {
53
+ return {
54
+ code: "PERMISSION_DENIED",
55
+ message: "Only the service key can manage users."
56
+ };
57
+ }
52
58
  function cryptoUnavailable() {
53
59
  return {
54
60
  code: "CRYPTO_UNAVAILABLE",
@@ -128,9 +134,10 @@ async function call(config, options) {
128
134
  const init = {
129
135
  method: options.method,
130
136
  headers,
131
- // Identity is the bearer token alone. Better Auth also sets a session
132
- // cookie (spec §13); a request that carried it would let any site act
133
- // as the user. Workers accept the field (measured on workerd 1.20260815).
137
+ // Identity is the bearer token alone. The sign-in routes also set a
138
+ // session cookie (spec §13); a request that carried it would let any site
139
+ // act as the user. Workers accept the field (measured on workerd
140
+ // 1.20260815).
134
141
  credentials: "omit"
135
142
  };
136
143
  if (payload !== void 0) {
@@ -151,15 +158,65 @@ async function call(config, options) {
151
158
  const answer = { status, headers: responseHeaders, body, token };
152
159
  const error = errorFromBody(status, body);
153
160
  if (error === null) return { error: null, answer };
161
+ return {
162
+ error: await settled(config, error, responseHeaders, token),
163
+ answer
164
+ };
165
+ }
166
+ async function settled(config, error, headers, token) {
154
167
  if (error.code === "INVALID_SESSION" && token !== null)
155
168
  await config.tokens.clearIf(token);
156
- if (RETRY_AFTER_CODES.has(error.code))
157
- return { error: withRetryAfter(error, responseHeaders), answer };
158
- return { error, answer };
169
+ return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
170
+ }
171
+ async function send(config, options) {
172
+ const token = options.withToken ? await config.tokens.get() : null;
173
+ const headers = {
174
+ ...options.headers,
175
+ apikey: config.apiKey
176
+ };
177
+ if (token !== null) headers.Authorization = `Bearer ${token}`;
178
+ const init = {
179
+ method: options.method,
180
+ headers,
181
+ credentials: "omit"
182
+ };
183
+ if (options.body !== void 0) init.body = options.body;
184
+ let response;
185
+ try {
186
+ const fetchImpl = config.fetch ?? globalThis.fetch;
187
+ response = await fetchImpl(
188
+ `${config.baseUrl}/_naro/v1${options.path}`,
189
+ init
190
+ );
191
+ } catch (error2) {
192
+ return { error: networkError(error2) };
193
+ }
194
+ if (response.status >= 200 && response.status < 300)
195
+ return { error: null, response };
196
+ let text;
197
+ try {
198
+ text = await response.text();
199
+ } catch (error2) {
200
+ return { error: networkError(error2) };
201
+ }
202
+ let body;
203
+ if (text.trim() !== "") {
204
+ try {
205
+ body = JSON.parse(text);
206
+ } catch {
207
+ return { error: invalidResponse(response.status) };
208
+ }
209
+ }
210
+ const error = errorFromBody(response.status, body) ?? invalidResponse(response.status);
211
+ return { error: await settled(config, error, response.headers, token) };
159
212
  }
160
213
  var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
161
214
  "RATE_LIMITED",
162
- "WRITE_QUOTA_EXCEEDED"
215
+ "WRITE_QUOTA_EXCEEDED",
216
+ // Storage (original spec §13): one object written twice within a second
217
+ // (`x-retry-after: 1`), and the daily operation limit (3-1d).
218
+ "OBJECT_RATE_LIMITED",
219
+ "STORAGE_OPS_QUOTA_EXCEEDED"
163
220
  ]);
164
221
  function withRetryAfter(error, headers) {
165
222
  const header = headers.get("x-retry-after")?.trim() ?? "";
@@ -196,6 +253,90 @@ function isKnownServer(scope = globalThis) {
196
253
  return hasServerSignal(g) && !hasDeviceSignal(g);
197
254
  }
198
255
 
256
+ // src/auth/admin.ts
257
+ var USERS_PATH = "/auth/admin/users";
258
+ function dataOf(result) {
259
+ if (result.error !== null) return null;
260
+ const { body } = result.answer;
261
+ return isRecord(body) && isRecord(body.data) ? body.data : null;
262
+ }
263
+ function errorOf(result) {
264
+ if (result.error !== null) return result.error;
265
+ return invalidResponse(result.answer.status);
266
+ }
267
+ function userOf(value) {
268
+ return isRecord(value) && typeof value.id === "string" ? value : null;
269
+ }
270
+ var AuthAdminApi = class {
271
+ #http;
272
+ #serviceKey;
273
+ constructor(http, serviceKey) {
274
+ this.#http = http;
275
+ this.#serviceKey = serviceKey;
276
+ }
277
+ /**
278
+ * Creates a user with a password: an email, a username or both. It is
279
+ * made while sign-ups are closed too, and is confirmed unless
280
+ * `emailConfirm: false`. A username-only user answers `email: null` and
281
+ * signs in with `signIn({ username, password })`. Refusals come as the
282
+ * worker says them: USERNAME_TAKEN (409),
283
+ * USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL (422), PASSWORD_TOO_SHORT ("Use at
284
+ * least {n} characters."), VALIDATION_ERROR, WRITE_QUOTA_EXCEEDED (429).
285
+ */
286
+ async createUser(attributes) {
287
+ if (!this.#serviceKey)
288
+ return { data: { user: null }, error: serviceKeyRequired() };
289
+ const result = await call(this.#http, {
290
+ method: "POST",
291
+ path: USERS_PATH,
292
+ body: attributes,
293
+ withToken: false
294
+ });
295
+ const user = userOf(dataOf(result)?.user);
296
+ if (user === null) return { data: { user: null }, error: errorOf(result) };
297
+ return { data: { user }, error: null };
298
+ }
299
+ /** The project's users, newest first: page 1 and 50 a page unless given. */
300
+ async listUsers(params = {}) {
301
+ const none = { users: [], total: 0 };
302
+ if (!this.#serviceKey) return { data: none, error: serviceKeyRequired() };
303
+ const query = new URLSearchParams();
304
+ if (params.page !== void 0) query.set("page", String(params.page));
305
+ if (params.perPage !== void 0)
306
+ query.set("perPage", String(params.perPage));
307
+ const search = query.toString();
308
+ const result = await call(this.#http, {
309
+ method: "GET",
310
+ path: search === "" ? USERS_PATH : `${USERS_PATH}?${search}`,
311
+ withToken: false
312
+ });
313
+ const data = dataOf(result);
314
+ const users = Array.isArray(data?.users) ? data.users.map(userOf) : null;
315
+ if (!data || users === null || users.includes(null) || typeof data.total !== "number")
316
+ return { data: none, error: errorOf(result) };
317
+ return {
318
+ data: { users, total: data.total },
319
+ error: null
320
+ };
321
+ }
322
+ /**
323
+ * Deletes a user and its sign-in methods, and ends its sessions. Answers
324
+ * the user it was; USER_NOT_FOUND (404) for an id that is no user.
325
+ */
326
+ async deleteUser(id) {
327
+ if (!this.#serviceKey)
328
+ return { data: { user: null }, error: serviceKeyRequired() };
329
+ const result = await call(this.#http, {
330
+ method: "DELETE",
331
+ path: `${USERS_PATH}/${encodeURIComponent(String(id))}`,
332
+ withToken: false
333
+ });
334
+ const user = userOf(dataOf(result)?.user);
335
+ if (user === null) return { data: { user: null }, error: errorOf(result) };
336
+ return { data: { user }, error: null };
337
+ }
338
+ };
339
+
199
340
  // src/auth/flow-errors.ts
200
341
  var CODE = /^[A-Z0-9_]{1,64}$/;
201
342
  var INVALID_CONFIRMATION = "The confirmation link is not valid. Ask for a new one.";
@@ -207,6 +348,9 @@ var MESSAGES = {
207
348
  OAUTH_ACCOUNT_NOT_LINKED: "An account with this email already exists and is not linked to this provider. Sign in the way you signed up.",
208
349
  PROVIDER_NOT_CONFIGURED: "Signing in with this provider is not set up for this project.",
209
350
  RATE_LIMITED: "Too many sign-in attempts. Wait a few seconds and try again.",
351
+ // A first Google or GitHub sign-in while the project's sign-ups are
352
+ // closed (spec 2026-09-26 §2.1): the worker's own words.
353
+ SIGNUPS_DISABLED: "Sign-ups are closed for this project.",
210
354
  INTERNAL_ERROR: "Something went wrong. Try again.",
211
355
  PASSWORD_RESET_REQUIRED: "Your email is confirmed. Someone else also signed up with it before it was confirmed, so its password was cleared: choose a new one with a password reset.",
212
356
  TOKEN_EXPIRED: "The confirmation link has expired. Ask for a new one.",
@@ -326,6 +470,12 @@ function share(shared, pending) {
326
470
  });
327
471
  }
328
472
  var AuthClient = class {
473
+ /**
474
+ * The service key's user management — createUser, listUsers, deleteUser,
475
+ * as in supabase-js (spec 2026-09-26 §2.2). Only on a client made with
476
+ * `serviceKey`, on a server.
477
+ */
478
+ admin;
329
479
  #http;
330
480
  #flow;
331
481
  /** The last `getSessionFromUrl()`, for callers after detection ran it. */
@@ -333,6 +483,7 @@ var AuthClient = class {
333
483
  constructor(http, flow) {
334
484
  this.#http = http;
335
485
  this.#flow = flow;
486
+ this.admin = new AuthAdminApi(http, flow.serviceKey);
336
487
  if (flow.detectSessionInUrl) {
337
488
  if (naroFragment() !== null) void this.getSessionFromUrl();
338
489
  else this.#fromUrl = sharedInFlight(flow.shared);
@@ -342,18 +493,37 @@ var AuthClient = class {
342
493
  * Creates the account and signs in; the token is stored for later calls.
343
494
  * When the project requires a confirmed email there is no session yet —
344
495
  * `user` comes back, and after the confirmation link the user signs in.
496
+ * While the project's sign-ups are closed every sign-up answers 403
497
+ * SIGNUPS_DISABLED; while they are open, a taken `username` answers 409
498
+ * USERNAME_TAKEN.
345
499
  */
346
500
  async signUp(credentials) {
347
- const { email, password } = credentials;
348
- const name = credentials.name ?? defaultName(email);
501
+ const { email, password, username } = credentials;
502
+ const name = credentials.name ?? username ?? defaultName(email);
349
503
  const body = { email, password, name };
504
+ if (username !== void 0) body.username = username;
350
505
  if (credentials.redirectTo !== void 0)
351
506
  body.callbackURL = credentials.redirectTo;
352
507
  return this.#signIn("/auth/sign-up/email", body, false);
353
508
  }
509
+ /**
510
+ * Signs in with an email and password, or a username and password (spec
511
+ * 2026-09-26 §2.3); the token is stored for later calls. A wrong password
512
+ * answers 401 INVALID_EMAIL_OR_PASSWORD, or INVALID_USERNAME_OR_PASSWORD.
513
+ */
354
514
  async signIn(credentials) {
355
- const { email, password } = credentials;
356
- return this.#signIn("/auth/sign-in/email", { email, password }, true);
515
+ const { password } = credentials;
516
+ if (credentials.username !== void 0)
517
+ return this.#signIn(
518
+ "/auth/sign-in/username",
519
+ { username: credentials.username, password },
520
+ true
521
+ );
522
+ return this.#signIn(
523
+ "/auth/sign-in/email",
524
+ { email: credentials.email, password },
525
+ true
526
+ );
357
527
  }
358
528
  /**
359
529
  * Starts a Google or GitHub sign-in: sends the browser, with a top-level
@@ -1636,6 +1806,379 @@ var TableBuilder = class {
1636
1806
  }
1637
1807
  };
1638
1808
 
1809
+ // src/storage/path.ts
1810
+ var MAX_SINGLE_UPLOAD_BYTES = 52428800;
1811
+ var STORAGE_MESSAGES = {
1812
+ bucket: "The bucket name is not valid.",
1813
+ tooLarge: "Uploads over 50 MiB are not supported yet.",
1814
+ request: "The storage request is not valid.",
1815
+ body: "upload takes a Blob, File, ArrayBuffer, typed array or string."
1816
+ };
1817
+ var RESERVED_BUCKETS = /* @__PURE__ */ new Set([
1818
+ "authenticated",
1819
+ "copy",
1820
+ "info",
1821
+ "list",
1822
+ "move",
1823
+ "public",
1824
+ "render",
1825
+ "sign",
1826
+ "upload"
1827
+ ]);
1828
+ var DOWNLOAD_NAME_REASON = "must be a file name of up to 255 bytes";
1829
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
1830
+ var OCTET_STREAM = "application/octet-stream";
1831
+ var UTF8_TEXT = "text/plain;charset=utf-8";
1832
+ function invalidPath(reason) {
1833
+ return {
1834
+ code: "INVALID_PATH",
1835
+ message: `The object path is not valid: it ${reason}.`
1836
+ };
1837
+ }
1838
+ function objectTooLarge() {
1839
+ return { code: "OBJECT_TOO_LARGE", message: STORAGE_MESSAGES.tooLarge };
1840
+ }
1841
+ function encodePath(path) {
1842
+ if (typeof path !== "string" || path === "")
1843
+ return { ok: false, error: invalidPath("is empty") };
1844
+ if (LONE_SURROGATE.test(path))
1845
+ return { ok: false, error: invalidPath("is not valid UTF-8") };
1846
+ if (path.startsWith("/") || path.endsWith("/"))
1847
+ return { ok: false, error: invalidPath("starts or ends with /") };
1848
+ if (path.includes("//"))
1849
+ return { ok: false, error: invalidPath("has an empty segment (//)") };
1850
+ const segments = path.split("/");
1851
+ if (segments.some((segment) => segment === "." || segment === ".."))
1852
+ return { ok: false, error: invalidPath("has a . or .. segment") };
1853
+ return { ok: true, encoded: segments.map(encodeURIComponent).join("/") };
1854
+ }
1855
+ function downloadQuery(download) {
1856
+ if (download === true) return { ok: true, encoded: "?download" };
1857
+ if (typeof download !== "string") return { ok: true, encoded: "" };
1858
+ if (LONE_SURROGATE.test(download))
1859
+ return {
1860
+ ok: false,
1861
+ error: {
1862
+ code: "INVALID_REQUEST",
1863
+ message: STORAGE_MESSAGES.request,
1864
+ details: { path: "download", reason: DOWNLOAD_NAME_REASON }
1865
+ }
1866
+ };
1867
+ return { ok: true, encoded: `?download=${encodeURIComponent(download)}` };
1868
+ }
1869
+ function encodeBucket(bucket) {
1870
+ if (typeof bucket !== "string" || bucket === "" || bucket === "." || bucket === ".." || LONE_SURROGATE.test(bucket) || RESERVED_BUCKETS.has(bucket))
1871
+ return {
1872
+ ok: false,
1873
+ error: { code: "INVALID_PATH", message: STORAGE_MESSAGES.bucket }
1874
+ };
1875
+ return { ok: true, encoded: encodeURIComponent(bucket) };
1876
+ }
1877
+ var isArrayBuffer = (value) => Object.prototype.toString.call(value) === "[object ArrayBuffer]";
1878
+ function isDetached(buffer) {
1879
+ if (!isArrayBuffer(buffer)) return false;
1880
+ const flag = buffer.detached;
1881
+ if (typeof flag === "boolean") return flag;
1882
+ if (buffer.byteLength > 0) return false;
1883
+ try {
1884
+ buffer.slice(0);
1885
+ return false;
1886
+ } catch {
1887
+ return true;
1888
+ }
1889
+ }
1890
+ var CACHE_CONTROL_REASON = "must be max-age=<seconds> from 0 to 31536000";
1891
+ var MAX_TYPE_IN_MESSAGE = 256;
1892
+ function unsendable(value) {
1893
+ return /[\0\r\n]|[^\0-\xff]/.test(
1894
+ value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "")
1895
+ );
1896
+ }
1897
+ function headerProblem(contentType, cacheControl) {
1898
+ if (typeof contentType === "string" && unsendable(contentType)) {
1899
+ let shown = contentType;
1900
+ if (shown.length > MAX_TYPE_IN_MESSAGE) {
1901
+ shown = shown.slice(0, MAX_TYPE_IN_MESSAGE);
1902
+ if (/[\ud800-\udbff]$/.test(shown)) shown = shown.slice(0, -1);
1903
+ }
1904
+ return {
1905
+ code: "INVALID_CONTENT_TYPE",
1906
+ message: `Content-Type ${shown} is not a media type like image/png.`
1907
+ };
1908
+ }
1909
+ if (unsendable(cacheControl))
1910
+ return {
1911
+ code: "INVALID_REQUEST",
1912
+ message: STORAGE_MESSAGES.request,
1913
+ details: { path: "cache-control", reason: CACHE_CONTROL_REASON }
1914
+ };
1915
+ return null;
1916
+ }
1917
+ function payloadOf(body, contentType) {
1918
+ if (typeof body === "string") {
1919
+ const bytes = new TextEncoder().encode(body);
1920
+ return {
1921
+ ok: true,
1922
+ body: bytes,
1923
+ size: bytes.byteLength,
1924
+ type: contentType ?? UTF8_TEXT
1925
+ };
1926
+ }
1927
+ if (typeof Blob === "function" && body instanceof Blob)
1928
+ return {
1929
+ ok: true,
1930
+ body,
1931
+ size: body.size,
1932
+ type: contentType ?? (body.type === "" ? OCTET_STREAM : body.type)
1933
+ };
1934
+ if (isArrayBuffer(body) && !isDetached(body))
1935
+ return {
1936
+ ok: true,
1937
+ body,
1938
+ size: body.byteLength,
1939
+ type: contentType ?? OCTET_STREAM
1940
+ };
1941
+ if (ArrayBuffer.isView(body) && !isDetached(body.buffer)) {
1942
+ const { buffer, byteOffset, byteLength } = body;
1943
+ const bytes = isArrayBuffer(buffer) ? new Uint8Array(buffer, byteOffset, byteLength) : Uint8Array.from(new Uint8Array(buffer, byteOffset, byteLength));
1944
+ return {
1945
+ ok: true,
1946
+ body: bytes,
1947
+ size: byteLength,
1948
+ type: contentType ?? OCTET_STREAM
1949
+ };
1950
+ }
1951
+ return {
1952
+ ok: false,
1953
+ error: { code: "INVALID_BODY", message: STORAGE_MESSAGES.body }
1954
+ };
1955
+ }
1956
+
1957
+ // src/storage/client.ts
1958
+ var SIGNED_PREFIX = "/_naro/v1/storage/object/sign/";
1959
+ function fail(error) {
1960
+ return { data: null, error };
1961
+ }
1962
+ var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
1963
+ var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
1964
+ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
1965
+ var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
1966
+ function listOf(guard) {
1967
+ return (value) => Array.isArray(value) && value.every(guard);
1968
+ }
1969
+ function dataOf2(status, body, guard) {
1970
+ if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
1971
+ return { data: body.data, error: null };
1972
+ return fail(invalidResponse(status));
1973
+ }
1974
+ async function readJson(response, guard) {
1975
+ let text;
1976
+ try {
1977
+ text = await response.text();
1978
+ } catch (error) {
1979
+ return fail(networkError(error));
1980
+ }
1981
+ let body;
1982
+ try {
1983
+ body = JSON.parse(text);
1984
+ } catch {
1985
+ return fail(invalidResponse(response.status));
1986
+ }
1987
+ return dataOf2(response.status, body, guard);
1988
+ }
1989
+ var StorageFileApi = class {
1990
+ #http;
1991
+ #auth;
1992
+ #bucket;
1993
+ constructor(http, auth, bucket) {
1994
+ this.#http = http;
1995
+ this.#auth = auth;
1996
+ this.#bucket = bucket;
1997
+ }
1998
+ /** `/storage/object/<route><bucket>/<path>`, or the refusal no request needs. */
1999
+ #objectPath(route, path) {
2000
+ const bucket = encodeBucket(this.#bucket);
2001
+ if (!bucket.ok) return bucket;
2002
+ const name = encodePath(path);
2003
+ if (!name.ok) return name;
2004
+ return {
2005
+ ok: true,
2006
+ path: `/storage/object/${route}${bucket.encoded}/${name.encoded}`
2007
+ };
2008
+ }
2009
+ /** `/storage/object/<route><bucket>`, for the routes that take names in the body. */
2010
+ #bucketPath(route) {
2011
+ const bucket = encodeBucket(this.#bucket);
2012
+ if (!bucket.ok) return bucket;
2013
+ return { ok: true, path: `/storage/object/${route}${bucket.encoded}` };
2014
+ }
2015
+ async #json(request, guard) {
2016
+ await this.#auth.ready();
2017
+ const result = await call(this.#http, { ...request, withToken: true });
2018
+ if (result.error !== null) return fail(result.error);
2019
+ return dataOf2(result.answer.status, result.answer.body, guard);
2020
+ }
2021
+ async #write(method, path, body, options, upsert) {
2022
+ const target = this.#objectPath("", path);
2023
+ if (!target.ok) return fail(target.error);
2024
+ const payload = payloadOf(body, options.contentType);
2025
+ if (!payload.ok) return fail(payload.error);
2026
+ const cache = `max-age=${options.cacheControl ?? "3600"}`;
2027
+ const unreadable = headerProblem(options.contentType, cache);
2028
+ if (unreadable !== null) return fail(unreadable);
2029
+ if (payload.size > MAX_SINGLE_UPLOAD_BYTES) return fail(objectTooLarge());
2030
+ await this.#auth.ready();
2031
+ const headers = {
2032
+ "content-type": payload.type,
2033
+ "cache-control": cache
2034
+ };
2035
+ if (upsert) headers["x-upsert"] = "true";
2036
+ const sent = await send(this.#http, {
2037
+ method,
2038
+ path: target.path,
2039
+ headers,
2040
+ body: payload.body,
2041
+ withToken: true
2042
+ });
2043
+ if (sent.error !== null) return fail(sent.error);
2044
+ return readJson(sent.response, isUploadResult);
2045
+ }
2046
+ /**
2047
+ * A new object; with `upsert: true`, one of the same name is replaced.
2048
+ * `null` options, as a JS caller may pass, are no options.
2049
+ */
2050
+ upload(path, body, options = {}) {
2051
+ const given = options ?? {};
2052
+ return this.#write("POST", path, body, given, given.upsert === true);
2053
+ }
2054
+ /** Replaces an object that is there (`OBJECT_NOT_FOUND` otherwise). */
2055
+ update(path, body, options = {}) {
2056
+ return this.#write("PUT", path, body, options ?? {}, false);
2057
+ }
2058
+ /**
2059
+ * The object's bytes, as a Blob of the type the worker serves it under:
2060
+ * the stored type for the images, audio, video, PDF, plain text, CSV,
2061
+ * JSON and SVG it serves as themselves, `application/octet-stream` for
2062
+ * anything else (HTML, scripts, archives, unknown types — `info` and
2063
+ * `list` hold the stored type).
2064
+ */
2065
+ async download(path) {
2066
+ const target = this.#objectPath("", path);
2067
+ if (!target.ok) return fail(target.error);
2068
+ await this.#auth.ready();
2069
+ const sent = await send(this.#http, {
2070
+ method: "GET",
2071
+ path: target.path,
2072
+ withToken: true
2073
+ });
2074
+ if (sent.error !== null) return fail(sent.error);
2075
+ try {
2076
+ return { data: await sent.response.blob(), error: null };
2077
+ } catch (error) {
2078
+ return fail(networkError(error));
2079
+ }
2080
+ }
2081
+ async info(path) {
2082
+ const target = this.#objectPath("info/", path);
2083
+ if (!target.ok) return fail(target.error);
2084
+ return this.#json({ method: "GET", path: target.path }, isFileObject);
2085
+ }
2086
+ /**
2087
+ * Whether the caller can see the object: `OBJECT_NOT_FOUND` is false,
2088
+ * every other error comes back as it is (plan C1 — asked with `info`,
2089
+ * not HEAD, whose answer has no body to tell a missing bucket from a
2090
+ * missing object).
2091
+ */
2092
+ async exists(path) {
2093
+ const found = await this.info(path);
2094
+ if (found.error === null) return { data: true, error: null };
2095
+ if (found.error.code === "OBJECT_NOT_FOUND")
2096
+ return { data: false, error: null };
2097
+ return fail(found.error);
2098
+ }
2099
+ /**
2100
+ * What is directly below `prefix` (the whole bucket by default): folders
2101
+ * first, then files. The options go to the worker as they are given.
2102
+ */
2103
+ async list(prefix, options = {}) {
2104
+ const target = this.#bucketPath("list/");
2105
+ if (!target.ok) return fail(target.error);
2106
+ return this.#json(
2107
+ {
2108
+ method: "POST",
2109
+ path: target.path,
2110
+ body: { ...prefix === void 0 ? {} : { prefix }, ...options }
2111
+ },
2112
+ listOf(isEntry2)
2113
+ );
2114
+ }
2115
+ /** Removes the objects named — 1 to 1,000 — and answers the ones it removed. */
2116
+ async remove(paths) {
2117
+ const target = this.#bucketPath("");
2118
+ if (!target.ok) return fail(target.error);
2119
+ return this.#json(
2120
+ { method: "DELETE", path: target.path, body: { prefixes: paths } },
2121
+ listOf(isFileObject)
2122
+ );
2123
+ }
2124
+ /**
2125
+ * A link to the object for `expiresIn` seconds (1–604,800). It stops
2126
+ * working once the object is replaced or removed.
2127
+ */
2128
+ async createSignedUrl(path, expiresIn, options = {}) {
2129
+ const target = this.#objectPath("sign/", path);
2130
+ if (!target.ok) return fail(target.error);
2131
+ const { download } = options ?? {};
2132
+ const signed = await this.#json(
2133
+ {
2134
+ method: "POST",
2135
+ path: target.path,
2136
+ body: {
2137
+ expiresIn,
2138
+ ...download === void 0 || download === false ? {} : { download }
2139
+ }
2140
+ },
2141
+ isSigned
2142
+ );
2143
+ if (signed.error !== null) return signed;
2144
+ return {
2145
+ data: { signedUrl: `${this.#http.baseUrl}${signed.data.signedUrl}` },
2146
+ error: null
2147
+ };
2148
+ }
2149
+ /**
2150
+ * A public bucket's URL for the object. No request is made, so whether
2151
+ * the bucket is public is not checked (as in supabase-js); a path no URL
2152
+ * can carry is INVALID_PATH (plan C3), and a download name none can carry
2153
+ * is the worker's INVALID_REQUEST for it.
2154
+ */
2155
+ getPublicUrl(path, options = {}) {
2156
+ const target = this.#objectPath("public/", path);
2157
+ if (!target.ok) return fail(target.error);
2158
+ const { download } = options ?? {};
2159
+ const query = downloadQuery(download);
2160
+ if (!query.ok) return fail(query.error);
2161
+ return {
2162
+ data: {
2163
+ publicUrl: `${this.#http.baseUrl}/_naro/v1${target.path}${query.encoded}`
2164
+ },
2165
+ error: null
2166
+ };
2167
+ }
2168
+ };
2169
+ var StorageClient = class {
2170
+ #http;
2171
+ #auth;
2172
+ constructor(http, auth) {
2173
+ this.#http = http;
2174
+ this.#auth = auth;
2175
+ }
2176
+ /** The objects of one bucket. */
2177
+ from(bucket) {
2178
+ return new StorageFileApi(this.#http, this.#auth, bucket);
2179
+ }
2180
+ };
2181
+
1639
2182
  // src/client.ts
1640
2183
  var CONFIG_MESSAGES = {
1641
2184
  url: "createClient needs the project url, like https://my-app.naro.sh.",
@@ -1668,9 +2211,10 @@ function apiKeyOf(options) {
1668
2211
  if (hasAnon && hasService) throw new Error(CONFIG_MESSAGES.bothKeys);
1669
2212
  const key = hasService ? serviceKey : hasAnon ? anonKey : void 0;
1670
2213
  if (key === void 0) throw new Error(CONFIG_MESSAGES.key);
1671
- if ((hasService || key.startsWith(SERVICE_KEY_PREFIX)) && !isKnownServer())
2214
+ const service = hasService || key.startsWith(SERVICE_KEY_PREFIX);
2215
+ if (service && !isKnownServer())
1672
2216
  throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
1673
- return key;
2217
+ return { key, service };
1674
2218
  }
1675
2219
  function queryRunner(http, auth) {
1676
2220
  return async (query) => {
@@ -1689,14 +2233,21 @@ function queryRunner(http, auth) {
1689
2233
  };
1690
2234
  }
1691
2235
  var NaroClient = class {
1692
- /** Sign-up, sign-in, sign-out and the current user (spec §13). */
2236
+ /**
2237
+ * Sign-up, sign-in, sign-out and the current user (spec §13); with the
2238
+ * service key, `auth.admin` manages users.
2239
+ */
1693
2240
  auth;
2241
+ /**
2242
+ * Files in the project's Naro Storage (first-release design 2026-10-06
2243
+ * §8.1): `storage.from(bucket)` uploads, downloads, lists and removes
2244
+ * them, as supabase-js does.
2245
+ */
2246
+ storage;
1694
2247
  #run;
1695
- // naro.storage — Phase 3 (spec §18): R2 storage lands here, beside `auth`
1696
- // and `from()`. Deliberately absent in Phase 1: not implemented, not typed.
1697
2248
  constructor(options) {
1698
2249
  const baseUrl = baseUrlOf(options.url);
1699
- const apiKey = apiKeyOf(options);
2250
+ const { key: apiKey, service } = apiKeyOf(options);
1700
2251
  if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
1701
2252
  throw new Error(CONFIG_MESSAGES.fetch);
1702
2253
  const storage = options.auth?.storage ?? defaultStorage();
@@ -1714,9 +2265,11 @@ var NaroClient = class {
1714
2265
  new TokenStore(storage, recoveryKey(baseUrl))
1715
2266
  ),
1716
2267
  detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
1717
- shared: { storage, key: storageKey(baseUrl) }
2268
+ shared: { storage, key: storageKey(baseUrl) },
2269
+ serviceKey: service
1718
2270
  });
1719
2271
  this.#run = queryRunner(http, this.auth);
2272
+ this.storage = new StorageClient(http, this.auth);
1720
2273
  }
1721
2274
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
1722
2275
  from(table) {