@omelhorsite/sdk 0.1.0 → 0.2.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/README.md CHANGED
@@ -89,6 +89,7 @@ const rows = await oms.http.get<{ id: string }[]>("/some/new/path");
89
89
  | `oms.account` | The signed-in user, their profile, their usage report. |
90
90
  | `oms.storage` | The virtual filesystem: nodes, uploads, downloads, grants. |
91
91
  | `oms.tools` | The metered media tools, each with its own daily quota. |
92
+ | `oms.quotas` | Every ceiling on the account - tools, storage and music - in one call. |
92
93
  | `oms.jobs` | Background jobs: list, get, wait, watch. The API has no cancel. |
93
94
  | `oms.tickets` | Support tickets and their message threads. |
94
95
  | `oms.shortLinks` `oms.notepads` `oms.dynamicQrs` `oms.chests` `oms.forms` `oms.linkTrees` | Everything that ends in a shareable URL. |
package/dist/index.js CHANGED
@@ -322,6 +322,7 @@ class ApiClient {
322
322
  baseUrl;
323
323
  fetchImpl;
324
324
  tokens;
325
+ sessionCookie;
325
326
  baseHeaders;
326
327
  timeoutMs;
327
328
  retry;
@@ -332,7 +333,11 @@ class ApiClient {
332
333
  throw new OmsNetworkError("No fetch implementation available. Pass one to the Oms constructor: new Oms({ fetch }).");
333
334
  }
334
335
  this.fetchImpl = (input, init) => injected(input, init);
336
+ if (options.sessionCookie && options.tokens) {
337
+ throw new TypeError("Pass either `sessionCookie` or a token, not both: two credentials on one request means the server decides which identity wins, and the caller cannot tell which one it got.");
338
+ }
335
339
  this.tokens = options.tokens;
340
+ this.sessionCookie = options.sessionCookie ?? false;
336
341
  this.baseHeaders = { ...options.headers ?? {} };
337
342
  if (options.clientName)
338
343
  this.baseHeaders["X-Oms-Client"] = options.clientName;
@@ -451,7 +456,7 @@ class ApiClient {
451
456
  headers,
452
457
  signal,
453
458
  ...body === undefined ? {} : { body },
454
- credentials: "omit",
459
+ credentials: this.sessionCookie ? "include" : "omit",
455
460
  redirect: "follow"
456
461
  };
457
462
  }
@@ -10476,6 +10481,45 @@ class NotepadsNamespace extends Resource {
10476
10481
  }
10477
10482
  }
10478
10483
 
10484
+ // src/resources/quotas.ts
10485
+ var QUOTA_RESOURCES = [
10486
+ "vocal_separation_seconds",
10487
+ "transcription_seconds",
10488
+ "caption_seconds",
10489
+ "jumpstyle_edits",
10490
+ "storage_nodes",
10491
+ "music_storage_bytes"
10492
+ ];
10493
+
10494
+ class QuotasNamespace extends Resource {
10495
+ async list(options = {}) {
10496
+ const answer = await this.http.get("/quotas", options);
10497
+ return {
10498
+ authenticated: answer?.authenticated === true,
10499
+ quotas: answer?.quotas ?? []
10500
+ };
10501
+ }
10502
+ async get(resource, options = {}) {
10503
+ return quotaFor(await this.list(options), resource) ?? null;
10504
+ }
10505
+ }
10506
+ function quotaFor(report, resource) {
10507
+ return report.quotas.find((entry) => entry.resource === resource);
10508
+ }
10509
+ function quotaExhausted(entry) {
10510
+ if (entry.unlimited || entry.remaining === null)
10511
+ return false;
10512
+ return entry.remaining <= 0;
10513
+ }
10514
+ function quotaAffords(entry, amount) {
10515
+ if (entry.unlimited)
10516
+ return true;
10517
+ const remaining = entry.remaining ?? (entry.limit === null ? null : Math.max(0, entry.limit - entry.used));
10518
+ if (remaining === null)
10519
+ return true;
10520
+ return amount <= remaining;
10521
+ }
10522
+
10479
10523
  // src/resources/storage.ts
10480
10524
  class FsGrantsNamespace extends Resource {
10481
10525
  async list(params = {}, options = {}) {
@@ -11389,17 +11433,22 @@ class Oms {
11389
11433
  forms;
11390
11434
  linkTrees;
11391
11435
  jobs;
11436
+ quotas;
11392
11437
  tools;
11393
11438
  local = local;
11394
11439
  constructor(options = {}) {
11395
11440
  if (options.tokens && options.token !== undefined && options.token !== null) {
11396
11441
  throw new TypeError("Pass either `token` or `tokens` to the Oms constructor, not both.");
11397
11442
  }
11443
+ if (options.sessionCookie && (options.tokens || options.token !== undefined && options.token !== null)) {
11444
+ throw new TypeError("Pass either `sessionCookie` or a token to the Oms constructor, not both.");
11445
+ }
11398
11446
  const tokens2 = options.tokens ?? providerFor(options.token);
11399
11447
  this.http = new ApiClient({
11400
11448
  ...options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl },
11401
11449
  ...options.fetch === undefined ? {} : { fetch: options.fetch },
11402
11450
  ...tokens2 === undefined ? {} : { tokens: tokens2 },
11451
+ ...options.sessionCookie === undefined ? {} : { sessionCookie: options.sessionCookie },
11403
11452
  ...options.headers === undefined ? {} : { headers: options.headers },
11404
11453
  ...options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs },
11405
11454
  ...options.retry === undefined ? {} : { retry: options.retry },
@@ -11417,6 +11466,7 @@ class Oms {
11417
11466
  this.forms = new FormsNamespace(this.http);
11418
11467
  this.linkTrees = new LinkTreesNamespace(this.http);
11419
11468
  this.jobs = new JobsNamespace(this.http);
11469
+ this.quotas = new QuotasNamespace(this.http);
11420
11470
  this.tools = new ToolsNamespace(this.http);
11421
11471
  }
11422
11472
  get baseUrl() {
@@ -11459,6 +11509,9 @@ export {
11459
11509
  readFileInput,
11460
11510
  readErrorBody,
11461
11511
  randomInt,
11512
+ quotaFor,
11513
+ quotaExhausted,
11514
+ quotaAffords,
11462
11515
  qrToSvg,
11463
11516
  qrToDataUri,
11464
11517
  pollUntilTerminal,
@@ -11524,6 +11577,8 @@ export {
11524
11577
  ShortLinksNamespace,
11525
11578
  SHORT_LINK_BASE_URL,
11526
11579
  Resource,
11580
+ QuotasNamespace,
11581
+ QUOTA_RESOURCES,
11527
11582
  POLL_BACKOFF_FACTOR,
11528
11583
  PART_URL_WINDOW,
11529
11584
  OmsTimeoutError,
@@ -21,6 +21,7 @@ import { IpLookupNamespace } from "./resources/ipLookup";
21
21
  import { JobsNamespace } from "./resources/jobs";
22
22
  import { LinkTreesNamespace } from "./resources/linkTrees";
23
23
  import { NotepadsNamespace } from "./resources/notepads";
24
+ import { QuotasNamespace } from "./resources/quotas";
24
25
  import { ShortLinksNamespace } from "./resources/shortLinks";
25
26
  import { StorageNamespace } from "./resources/storage";
26
27
  import { TicketsNamespace } from "./resources/tickets";
@@ -48,6 +49,18 @@ export interface OmsOptions {
48
49
  * passing both throws. Build one with `refreshingTokenProvider`.
49
50
  */
50
51
  readonly tokens?: TokenProvider;
52
+ /**
53
+ * Authenticate with the browser's httpOnly session cookie instead of a
54
+ * token. First-party pages only, and never the default: see
55
+ * {@link ApiClientOptions.sessionCookie} for why it has to be asked for
56
+ * by name.
57
+ *
58
+ * ```ts
59
+ * // in the omelhorsite web app, served from the same site as the API
60
+ * const oms = new Oms({ sessionCookie: true });
61
+ * ```
62
+ */
63
+ readonly sessionCookie?: boolean;
51
64
  /** API root. Defaults to `https://backend.omelhorsite.pt`. */
52
65
  readonly baseUrl?: string;
53
66
  /**
@@ -110,6 +123,8 @@ export declare class Oms {
110
123
  readonly linkTrees: LinkTreesNamespace;
111
124
  /** Background jobs: listing, polling and watching. There is no cancel. */
112
125
  readonly jobs: JobsNamespace;
126
+ /** Every ceiling on the account - tools, storage and music - in one call. */
127
+ readonly quotas: QuotasNamespace;
113
128
  /** The metered media tools, each with its own daily quota. */
114
129
  readonly tools: ToolsNamespace;
115
130
  /**
@@ -40,6 +40,23 @@ export interface ApiClientOptions {
40
40
  readonly fetch?: FetchLike;
41
41
  /** Where the bearer token comes from. */
42
42
  readonly tokens?: TokenProvider;
43
+ /**
44
+ * Authenticate with the browser's `oms_session` cookie instead of a token.
45
+ *
46
+ * FOR A FIRST-PARTY PAGE ONLY, and never the default. The cookie is httpOnly
47
+ * precisely so that no JavaScript can read the session token; a page served
48
+ * from omelhorsite.pt asks the browser to attach it, and the token itself
49
+ * stays out of reach of any script on the page.
50
+ *
51
+ * That is also why this has to be asked for by name. Left off, an SDK call is
52
+ * authenticated by the token it was handed and by nothing else, so a page
53
+ * that embeds the SDK cannot act as whoever happens to be signed in. Turning
54
+ * it on is a statement that this code IS the first-party app.
55
+ *
56
+ * Requires a same-site page: the cookie is host-only on the API host, so a
57
+ * page anywhere else gets an unauthenticated request, not an error.
58
+ */
59
+ readonly sessionCookie?: boolean;
43
60
  /** Headers merged into every request, below per-call headers. */
44
61
  readonly headers?: Record<string, string>;
45
62
  /** Default deadline for one call, retries included. `0` disables it. */
@@ -85,6 +102,7 @@ export declare class ApiClient {
85
102
  readonly baseUrl: string;
86
103
  private readonly fetchImpl;
87
104
  private readonly tokens;
105
+ private readonly sessionCookie;
88
106
  private readonly baseHeaders;
89
107
  private readonly timeoutMs;
90
108
  private readonly retry;
@@ -126,9 +126,12 @@ export interface AccountStorageUsage {
126
126
  /**
127
127
  * `GET /account/usage`: what the account has spent, per area.
128
128
  *
129
- * This is a bespoke report, not the daily tool quotas - each metered tool
130
- * reports its own ceiling through its `quota()` call. There is also a row
131
- * ceiling (250 000 nodes) that this report does not carry.
129
+ * This is a bespoke report, not a quota answer: it carries breakdowns nothing
130
+ * else has (the biggest files, the extension histogram) and it does NOT carry
131
+ * every ceiling - the row ceiling on the file tree and the music byte ceiling
132
+ * are absent from it. For ceilings, ask `oms.quotas.list()`, which answers all
133
+ * of them in one call; each metered tool also still reports its own through
134
+ * its `quota()`.
132
135
  */
133
136
  export interface AccountUsage {
134
137
  readonly user: {
@@ -23,6 +23,7 @@ export * from "./ipLookup";
23
23
  export * from "./jobs";
24
24
  export * from "./linkTrees";
25
25
  export * from "./notepads";
26
+ export * from "./quotas";
26
27
  export * from "./shortLinks";
27
28
  export * from "./storage";
28
29
  export * from "./storage/upload";
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The `quotas` namespace: every ceiling on the account, in one call.
3
+ *
4
+ * This exists because the question an agent actually asks is "how much is left
5
+ * of everything", and answering it used to mean knowing which four tools were
6
+ * metered, calling four endpoints, and then knowing that neither storage nor
7
+ * music was in any of those answers. `oms.quotas.list()` is one request and
8
+ * returns every resource the server meters, each in the same shape.
9
+ *
10
+ * The per-tool `quota()` calls have NOT gone anywhere and answer exactly what
11
+ * they always answered - they are published API. Use them when you want one
12
+ * tool and nothing else, or when the credential carries `tools:read` but not
13
+ * `profile`; use this when you want the picture.
14
+ *
15
+ * Two things to read before trusting a number:
16
+ *
17
+ * - **`period` is not decoration.** A `"daily"` resource resets at midnight,
18
+ * server time, and `used` is what today has spent. A `"total"` resource is
19
+ * what is stored RIGHT NOW and only falls when something is deleted; waiting
20
+ * does not give it back.
21
+ * - **Anonymous callers get a shorter list.** Without a credential the server
22
+ * answers with the daily resources only, counted per IP, because an
23
+ * anonymous caller has no file tree and no music library. Never index the
24
+ * array by position - look the resource up by name, or use
25
+ * {@link quotaFor}, which returns `undefined` rather than lying.
26
+ *
27
+ * Needs the `profile` scope for an OAuth token, the same scope
28
+ * `account.usage()` needs, because a quota is account state and it spans tools,
29
+ * storage and music at once.
30
+ *
31
+ * ```ts
32
+ * const report = await oms.quotas.list();
33
+ * const music = quotaFor(report, "music_storage_bytes");
34
+ * if (music && !quotaAffords(music, file.size)) throw new Error("no room");
35
+ * ```
36
+ */
37
+ import { Resource } from "../http";
38
+ import type { RequestOptions } from "../types";
39
+ /**
40
+ * Every resource the server's catalogue defines today, in the order it answers
41
+ * them: the four metered tools first, then the two storage ceilings.
42
+ *
43
+ * A server that grows a seventh keeps working - {@link QuotaEntry.resource} is
44
+ * widened to `string` on purpose, so an unknown name arrives as data rather
45
+ * than as a type error in a client nobody has rebuilt.
46
+ */
47
+ export declare const QUOTA_RESOURCES: readonly ["vocal_separation_seconds", "transcription_seconds", "caption_seconds", "jumpstyle_edits", "storage_nodes", "music_storage_bytes"];
48
+ /** One of {@link QUOTA_RESOURCES}. */
49
+ export type QuotaResource = (typeof QUOTA_RESOURCES)[number];
50
+ /**
51
+ * What the numbers count. `"seconds"` of media, `"count"` of whole things
52
+ * (edits, files and folders), `"bytes"` of stored media.
53
+ */
54
+ export type QuotaUnit = "seconds" | "count" | "bytes";
55
+ /**
56
+ * `"daily"` spends and resets at midnight, server time. `"total"` is what is
57
+ * stored right now and only falls when something is deleted.
58
+ */
59
+ export type QuotaPeriod = "daily" | "total";
60
+ /** One resource, in the one shape every resource uses. */
61
+ export interface QuotaEntry {
62
+ /** A {@link QuotaResource}, or a name added to the server since this build. */
63
+ readonly resource: QuotaResource | string;
64
+ readonly unit: QuotaUnit | string;
65
+ readonly period: QuotaPeriod | string;
66
+ /** Spent today for a daily resource; stored right now for a total one. */
67
+ readonly used: number;
68
+ /** `null` exactly when {@link unlimited} is `true`. */
69
+ readonly limit: number | null;
70
+ /** `null` exactly when {@link unlimited} is `true`. Never negative. */
71
+ readonly remaining: number | null;
72
+ /** `true` when this account has no ceiling on this resource. */
73
+ readonly unlimited: boolean;
74
+ }
75
+ /** `GET /quotas` in full. */
76
+ export interface QuotaReport {
77
+ /** Whether the caller was recognised. Anonymous callers get a shorter list. */
78
+ readonly authenticated: boolean;
79
+ /** One entry per resource the caller can actually spend. */
80
+ readonly quotas: QuotaEntry[];
81
+ }
82
+ /** The `quotas` namespace, reachable as `oms.quotas`. */
83
+ export declare class QuotasNamespace extends Resource {
84
+ /**
85
+ * `GET /quotas` - every ceiling on the account, in one request.
86
+ *
87
+ * Works anonymously, and then answers with the daily resources only.
88
+ *
89
+ * Every number is computed on the spot - the daily ones are aggregates over
90
+ * today's rows, and the two totals are a subtree row count and a blob-size
91
+ * sum - so this is a call to make BEFORE an expensive upload, not one to
92
+ * poll. The server throttles it at thirty a minute per caller, well above
93
+ * that use and well below a loop.
94
+ */
95
+ list(options?: RequestOptions): Promise<QuotaReport>;
96
+ /**
97
+ * One resource, or `null` when the caller cannot spend it - which is what an
98
+ * anonymous caller gets for `storage_nodes` and `music_storage_bytes`.
99
+ *
100
+ * Costs the same single request as {@link list}, because the server has no
101
+ * per-resource endpoint. Asking for three resources means calling
102
+ * {@link list} once and using {@link quotaFor} three times, not calling this
103
+ * three times.
104
+ */
105
+ get(resource: QuotaResource | string, options?: RequestOptions): Promise<QuotaEntry | null>;
106
+ }
107
+ /**
108
+ * Finds one resource in a report. Returns `undefined` when it is not there,
109
+ * which is a real answer and not a failure: an anonymous caller has no storage
110
+ * quota because they have no storage.
111
+ */
112
+ export declare function quotaFor(report: QuotaReport, resource: QuotaResource | string): QuotaEntry | undefined;
113
+ /** True when there is nothing left to spend. Always false when unlimited. */
114
+ export declare function quotaExhausted(entry: QuotaEntry): boolean;
115
+ /**
116
+ * Whether `amount` more fits under the ceiling, in the resource's own unit.
117
+ *
118
+ * Always true when unlimited. Uses `remaining` when the server sent one and
119
+ * falls back to `limit - used` when it did not, so it is honest about a report
120
+ * that is missing a field rather than reading `null` as zero.
121
+ */
122
+ export declare function quotaAffords(entry: QuotaEntry, amount: number): boolean;
@@ -9,7 +9,9 @@
9
9
  * - it is asynchronous: the create call answers with a row in `"pending"` and
10
10
  * the work happens on a sidecar;
11
11
  * - it has a daily quota, metered in seconds for the audio and video tools and
12
- * in edits for jumpstyle, and smaller for an anonymous caller;
12
+ * in edits for jumpstyle, and smaller for an anonymous caller - each tool's
13
+ * own `quota()` reads it, and `oms.quotas.list()` reads all of them plus the
14
+ * storage ceilings in one request;
13
15
  * - an anonymous caller must pass a Turnstile token
14
16
  * ({@link ToolCaptcha.captchaToken}, sent as `cf_turnstile_token`).
15
17
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omelhorsite/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript SDK for the omelhorsite API. Isolate-safe: no node builtins, no environment access, no stdout.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -17,11 +17,12 @@
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "typecheck": "tsc --noEmit -p .",
20
+ "typecheck": "tsc --noEmit -p . && tsc --noEmit -p tsconfig.test.json",
21
21
  "check:isolate": "bun run scripts/check-isolate.ts",
22
22
  "test": "bun run check:isolate && bun test",
23
23
  "build": "bun build src/index.ts --target node --format esm --outdir dist --external uqr && tsc -p tsconfig.build.json",
24
- "prepublishOnly": "bun run build"
24
+ "prepublishOnly": "bun run build",
25
+ "typecheck:tests": "tsc --noEmit -p tsconfig.test.json"
25
26
  },
26
27
  "devDependencies": {
27
28
  "typescript": "^5.9.3"
@@ -31,7 +32,6 @@
31
32
  },
32
33
  "repository": {
33
34
  "type": "git",
34
- "url": "git+https://github.com/afonsopc/omelhorsite.git",
35
- "directory": "apps/cli/packages/core"
35
+ "url": "git+https://github.com/afonsopc/omelhorsite-sdk.git"
36
36
  }
37
37
  }