@fluxfiles/node 0.1.1 → 0.1.3

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
@@ -48,6 +48,26 @@ const token = createToken({
48
48
  });
49
49
  ```
50
50
 
51
+ ### Enable Import from URL
52
+
53
+ Import-from-URL is **off by default**. Turn it on for a token by setting the
54
+ import options — no server-side per-tenant config is needed:
55
+
56
+ ```ts
57
+ const token = createToken({
58
+ userId: 'user-42',
59
+ perms: ['read', 'write'],
60
+ allowUrlImport: true, // required — enables the feature
61
+ maxImportMb: 20, // optional — cap per import (MB)
62
+ importUrlAllowlist: ['*.unsplash.com'], // optional — restrict source hosts
63
+ // importPath, importRateLimit, importConcurrency also supported
64
+ });
65
+ ```
66
+
67
+ The core then accepts `POST /api/fm/import-url` for that token (SSRF-guarded,
68
+ sharing the quota/dedup/variants pipeline). Server-wide defaults come from
69
+ `FLUXFILES_IMPORT_*` env vars on the core service.
70
+
51
71
  ### BYOB — encrypt a user's own bucket credentials
52
72
 
53
73
  ```ts
@@ -115,6 +135,8 @@ app.get('/fluxfiles/token', (req, res) => {
115
135
 
116
136
  `createToken` options: `secret?`, `userId`, `perms?`, `disks?`, `prefix?`,
117
137
  `maxUploadMb?`, `allowedExt?`, `ttl?`, `ownerOnly?`, `maxStorageMb?`, `maxFiles?`.
138
+ Per-tenant overrides (omit to inherit the server default): `aiAutoTag?` (bool),
139
+ `rateRead?` / `rateWrite?` (req/min), `variants?` (`{ thumb?, medium?, large? }` px).
118
140
  `createByobToken` replaces `disks` with `byobDisks` (a map of name → S3-compatible
119
141
  config) and does not take `maxStorageMb`/`maxFiles` (matching the core).
120
142
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- /** A FluxFiles permission. */
2
- type FluxPermission = 'read' | 'write' | 'delete';
1
+ /** A FluxFiles permission. `audit` gates reading the activity log. */
2
+ type FluxPermission = 'read' | 'write' | 'delete' | 'audit';
3
3
  /**
4
4
  * A BYOB (Bring Your Own Bucket) disk config. Encrypted into the JWT and
5
5
  * decrypted only at runtime by the FluxFiles server. Only S3-compatible
@@ -33,6 +33,25 @@ interface BaseTokenOptions {
33
33
  ttl?: number;
34
34
  /** Restrict destructive ops to files the user uploaded. */
35
35
  ownerOnly?: boolean;
36
+ /** Per-tenant AI auto-tag toggle. Omit to inherit the server default. */
37
+ aiAutoTag?: boolean;
38
+ /** Per-tenant read rate limit (requests/min). `0`/omitted = inherit server default. */
39
+ rateRead?: number;
40
+ /** Per-tenant write rate limit (requests/min). `0`/omitted = inherit server default. */
41
+ rateWrite?: number;
42
+ /** Per-tenant image variant widths, e.g. `{ thumb: 150, medium: 768, large: 1920 }`. Omit to inherit. */
43
+ variants?: Partial<Record<'thumb' | 'medium' | 'large', number>> | null;
44
+ /** Enable Import-from-URL for this tenant (`POST /api/fm/import-url`). Default off. */
45
+ allowUrlImport?: boolean;
46
+ /** Max size per URL import, in MB (same unit as `maxUploadMb`). `0`/omitted = inherit (50). */
47
+ maxImportMb?: number;
48
+ /** Restrict imports to these host globs, e.g. `['*.unsplash.com']`. Omit = any public host. */
49
+ importUrlAllowlist?: string[];
50
+ /** Force imports into this path, ignoring the request path. */
51
+ importPath?: string;
52
+ /** Import-specific rate limit (req/min) and max concurrent imports. `0`/omitted = inherit. */
53
+ importRateLimit?: number;
54
+ importConcurrency?: number;
36
55
  }
37
56
  interface CreateTokenOptions extends BaseTokenOptions {
38
57
  /** Disk names the token may access. */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** A FluxFiles permission. */
2
- type FluxPermission = 'read' | 'write' | 'delete';
1
+ /** A FluxFiles permission. `audit` gates reading the activity log. */
2
+ type FluxPermission = 'read' | 'write' | 'delete' | 'audit';
3
3
  /**
4
4
  * A BYOB (Bring Your Own Bucket) disk config. Encrypted into the JWT and
5
5
  * decrypted only at runtime by the FluxFiles server. Only S3-compatible
@@ -33,6 +33,25 @@ interface BaseTokenOptions {
33
33
  ttl?: number;
34
34
  /** Restrict destructive ops to files the user uploaded. */
35
35
  ownerOnly?: boolean;
36
+ /** Per-tenant AI auto-tag toggle. Omit to inherit the server default. */
37
+ aiAutoTag?: boolean;
38
+ /** Per-tenant read rate limit (requests/min). `0`/omitted = inherit server default. */
39
+ rateRead?: number;
40
+ /** Per-tenant write rate limit (requests/min). `0`/omitted = inherit server default. */
41
+ rateWrite?: number;
42
+ /** Per-tenant image variant widths, e.g. `{ thumb: 150, medium: 768, large: 1920 }`. Omit to inherit. */
43
+ variants?: Partial<Record<'thumb' | 'medium' | 'large', number>> | null;
44
+ /** Enable Import-from-URL for this tenant (`POST /api/fm/import-url`). Default off. */
45
+ allowUrlImport?: boolean;
46
+ /** Max size per URL import, in MB (same unit as `maxUploadMb`). `0`/omitted = inherit (50). */
47
+ maxImportMb?: number;
48
+ /** Restrict imports to these host globs, e.g. `['*.unsplash.com']`. Omit = any public host. */
49
+ importUrlAllowlist?: string[];
50
+ /** Force imports into this path, ignoring the request path. */
51
+ importPath?: string;
52
+ /** Import-specific rate limit (req/min) and max concurrent imports. `0`/omitted = inherit. */
53
+ importRateLimit?: number;
54
+ importConcurrency?: number;
36
55
  }
37
56
  interface CreateTokenOptions extends BaseTokenOptions {
38
57
  /** Disk names the token may access. */
package/dist/index.js CHANGED
@@ -98,6 +98,7 @@ function createToken(opts) {
98
98
  max_files: opts.maxFiles ?? 0
99
99
  };
100
100
  if (opts.ownerOnly) payload.owner_only = true;
101
+ applyTenantOverrides(payload, opts);
101
102
  return sign(payload, secret);
102
103
  }
103
104
  function createByobToken(opts) {
@@ -123,8 +124,33 @@ function createByobToken(opts) {
123
124
  byob_disks: encrypted
124
125
  };
125
126
  if (opts.ownerOnly) payload.owner_only = true;
127
+ applyTenantOverrides(payload, opts);
126
128
  return sign(payload, secret);
127
129
  }
130
+ function sanitizeVariants(v) {
131
+ if (!v || typeof v !== "object") return null;
132
+ const out = {};
133
+ for (const name of ["thumb", "medium", "large"]) {
134
+ const w = Math.trunc(Number(v[name]));
135
+ if (Number.isFinite(w) && w >= 16 && w <= 8e3) out[name] = w;
136
+ }
137
+ return Object.keys(out).length ? out : null;
138
+ }
139
+ function applyTenantOverrides(payload, opts) {
140
+ if (opts.aiAutoTag !== void 0) payload.ai_auto_tag = !!opts.aiAutoTag;
141
+ if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
142
+ if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
143
+ const variants = sanitizeVariants(opts.variants);
144
+ if (variants) payload.variants = variants;
145
+ if (opts.allowUrlImport) payload.allow_url_import = true;
146
+ if (opts.maxImportMb && opts.maxImportMb > 0) payload.max_import_mb = Math.trunc(opts.maxImportMb);
147
+ if (opts.importRateLimit && opts.importRateLimit > 0) payload.import_rate_limit = Math.trunc(opts.importRateLimit);
148
+ if (opts.importConcurrency && opts.importConcurrency > 0) payload.import_concurrency = Math.trunc(opts.importConcurrency);
149
+ if (opts.importPath) payload.import_path = String(opts.importPath);
150
+ if (Array.isArray(opts.importUrlAllowlist) && opts.importUrlAllowlist.length) {
151
+ payload.import_url_allowlist = opts.importUrlAllowlist.map((h) => String(h));
152
+ }
153
+ }
128
154
  function validateByobDisk(name, config) {
129
155
  if (!config || config.driver !== "s3") {
130
156
  throw new Error(`FluxFiles BYOB disk "${name}": driver must be "s3" (the server rejects "local").`);
package/dist/index.mjs CHANGED
@@ -76,6 +76,7 @@ function createToken(opts) {
76
76
  max_files: opts.maxFiles ?? 0
77
77
  };
78
78
  if (opts.ownerOnly) payload.owner_only = true;
79
+ applyTenantOverrides(payload, opts);
79
80
  return sign(payload, secret);
80
81
  }
81
82
  function createByobToken(opts) {
@@ -101,8 +102,33 @@ function createByobToken(opts) {
101
102
  byob_disks: encrypted
102
103
  };
103
104
  if (opts.ownerOnly) payload.owner_only = true;
105
+ applyTenantOverrides(payload, opts);
104
106
  return sign(payload, secret);
105
107
  }
108
+ function sanitizeVariants(v) {
109
+ if (!v || typeof v !== "object") return null;
110
+ const out = {};
111
+ for (const name of ["thumb", "medium", "large"]) {
112
+ const w = Math.trunc(Number(v[name]));
113
+ if (Number.isFinite(w) && w >= 16 && w <= 8e3) out[name] = w;
114
+ }
115
+ return Object.keys(out).length ? out : null;
116
+ }
117
+ function applyTenantOverrides(payload, opts) {
118
+ if (opts.aiAutoTag !== void 0) payload.ai_auto_tag = !!opts.aiAutoTag;
119
+ if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
120
+ if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
121
+ const variants = sanitizeVariants(opts.variants);
122
+ if (variants) payload.variants = variants;
123
+ if (opts.allowUrlImport) payload.allow_url_import = true;
124
+ if (opts.maxImportMb && opts.maxImportMb > 0) payload.max_import_mb = Math.trunc(opts.maxImportMb);
125
+ if (opts.importRateLimit && opts.importRateLimit > 0) payload.import_rate_limit = Math.trunc(opts.importRateLimit);
126
+ if (opts.importConcurrency && opts.importConcurrency > 0) payload.import_concurrency = Math.trunc(opts.importConcurrency);
127
+ if (opts.importPath) payload.import_path = String(opts.importPath);
128
+ if (Array.isArray(opts.importUrlAllowlist) && opts.importUrlAllowlist.length) {
129
+ payload.import_url_allowlist = opts.importUrlAllowlist.map((h) => String(h));
130
+ }
131
+ }
106
132
  function validateByobDisk(name, config) {
107
133
  if (!config || config.driver !== "s3") {
108
134
  throw new Error(`FluxFiles BYOB disk "${name}": driver must be "s3" (the server rejects "local").`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxfiles/node",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Server-side Node/TypeScript SDK for minting FluxFiles JWTs (plain + BYOB), byte-compatible with the PHP core",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "src"
19
+ "src",
20
+ "LICENSE"
20
21
  ],
21
22
  "engines": {
22
23
  "node": ">=16"
package/src/token.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { base64url, hmacSha256, encryptByob } from './crypto';
3
- import type { ByobDiskConfig, CreateByobTokenOptions, CreateTokenOptions } from './types';
3
+ import type { BaseTokenOptions, ByobDiskConfig, CreateByobTokenOptions, CreateTokenOptions } from './types';
4
4
 
5
5
  const MIN_SECRET_BYTES = 32;
6
6
 
@@ -48,6 +48,7 @@ export function createToken(opts: CreateTokenOptions): string {
48
48
  max_files: opts.maxFiles ?? 0,
49
49
  };
50
50
  if (opts.ownerOnly) payload.owner_only = true;
51
+ applyTenantOverrides(payload, opts);
51
52
  return sign(payload, secret);
52
53
  }
53
54
 
@@ -81,9 +82,40 @@ export function createByobToken(opts: CreateByobTokenOptions): string {
81
82
  byob_disks: encrypted,
82
83
  };
83
84
  if (opts.ownerOnly) payload.owner_only = true;
85
+ applyTenantOverrides(payload, opts);
84
86
  return sign(payload, secret);
85
87
  }
86
88
 
89
+ /** Sanitize the per-tenant `variants` claim — matches PHP `Claims::sanitizeVariants`. */
90
+ function sanitizeVariants(v: BaseTokenOptions['variants']): Record<string, number> | null {
91
+ if (!v || typeof v !== 'object') return null;
92
+ const out: Record<string, number> = {};
93
+ for (const name of ['thumb', 'medium', 'large'] as const) {
94
+ const w = Math.trunc(Number((v as Record<string, unknown>)[name]));
95
+ if (Number.isFinite(w) && w >= 16 && w <= 8000) out[name] = w;
96
+ }
97
+ return Object.keys(out).length ? out : null;
98
+ }
99
+
100
+ /** Copy the optional per-tenant override claims into a payload when set. */
101
+ function applyTenantOverrides(payload: Record<string, unknown>, opts: BaseTokenOptions): void {
102
+ if (opts.aiAutoTag !== undefined) payload.ai_auto_tag = !!opts.aiAutoTag;
103
+ if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
104
+ if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
105
+ const variants = sanitizeVariants(opts.variants);
106
+ if (variants) payload.variants = variants;
107
+
108
+ // URL-import claims (the server sanitizes/clamps these on decode).
109
+ if (opts.allowUrlImport) payload.allow_url_import = true;
110
+ if (opts.maxImportMb && opts.maxImportMb > 0) payload.max_import_mb = Math.trunc(opts.maxImportMb);
111
+ if (opts.importRateLimit && opts.importRateLimit > 0) payload.import_rate_limit = Math.trunc(opts.importRateLimit);
112
+ if (opts.importConcurrency && opts.importConcurrency > 0) payload.import_concurrency = Math.trunc(opts.importConcurrency);
113
+ if (opts.importPath) payload.import_path = String(opts.importPath);
114
+ if (Array.isArray(opts.importUrlAllowlist) && opts.importUrlAllowlist.length) {
115
+ payload.import_url_allowlist = opts.importUrlAllowlist.map((h) => String(h));
116
+ }
117
+ }
118
+
87
119
  /**
88
120
  * Light client-side validation. The server independently re-validates (incl.
89
121
  * SSRF checks on the endpoint), so this only catches obvious mistakes early.
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
- /** A FluxFiles permission. */
2
- export type FluxPermission = 'read' | 'write' | 'delete';
1
+ /** A FluxFiles permission. `audit` gates reading the activity log. */
2
+ export type FluxPermission = 'read' | 'write' | 'delete' | 'audit';
3
3
 
4
4
  /**
5
5
  * A BYOB (Bring Your Own Bucket) disk config. Encrypted into the JWT and
@@ -35,6 +35,25 @@ export interface BaseTokenOptions {
35
35
  ttl?: number;
36
36
  /** Restrict destructive ops to files the user uploaded. */
37
37
  ownerOnly?: boolean;
38
+ /** Per-tenant AI auto-tag toggle. Omit to inherit the server default. */
39
+ aiAutoTag?: boolean;
40
+ /** Per-tenant read rate limit (requests/min). `0`/omitted = inherit server default. */
41
+ rateRead?: number;
42
+ /** Per-tenant write rate limit (requests/min). `0`/omitted = inherit server default. */
43
+ rateWrite?: number;
44
+ /** Per-tenant image variant widths, e.g. `{ thumb: 150, medium: 768, large: 1920 }`. Omit to inherit. */
45
+ variants?: Partial<Record<'thumb' | 'medium' | 'large', number>> | null;
46
+ /** Enable Import-from-URL for this tenant (`POST /api/fm/import-url`). Default off. */
47
+ allowUrlImport?: boolean;
48
+ /** Max size per URL import, in MB (same unit as `maxUploadMb`). `0`/omitted = inherit (50). */
49
+ maxImportMb?: number;
50
+ /** Restrict imports to these host globs, e.g. `['*.unsplash.com']`. Omit = any public host. */
51
+ importUrlAllowlist?: string[];
52
+ /** Force imports into this path, ignoring the request path. */
53
+ importPath?: string;
54
+ /** Import-specific rate limit (req/min) and max concurrent imports. `0`/omitted = inherit. */
55
+ importRateLimit?: number;
56
+ importConcurrency?: number;
38
57
  }
39
58
 
40
59
  export interface CreateTokenOptions extends BaseTokenOptions {