@fluxfiles/node 0.1.26 → 0.1.27

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.d.mts CHANGED
@@ -143,6 +143,12 @@ interface BaseTokenOptions {
143
143
  /** Edition preset that defaults a tier's claims (`pro`/`agency`/`enterprise`).
144
144
  * DX sugar — explicit claim options still win, and the license gates the code. */
145
145
  edition?: 'free' | 'pro' | 'agency' | 'enterprise';
146
+ /** Role preset that defaults a person's capability level (perms, owner-scoping,
147
+ * and free power-user toggles) — orthogonal to `edition`, which defaults which
148
+ * *paid features* a tier gets. DX sugar — explicit claim options still win, and
149
+ * `role` never itself becomes a JWT claim. See docs/ACL-ROLE-PRESETS-DESIGN.md.
150
+ * Not supported on BYOB tokens (matches `edition`'s existing BYOB exclusion). */
151
+ role?: 'viewer' | 'editor' | 'admin' | 'superadmin';
146
152
  /** Paid-module claims (3-layer gate: code installed + licensed + this claim).
147
153
  * All default off; inert unless the module is installed & licensed on the server. */
148
154
  allowShare?: boolean;
@@ -157,6 +163,10 @@ interface BaseTokenOptions {
157
163
  /** Allow the share landing to render an inline preview (images via /api/fm/img;
158
164
  * PDFs only on uncapped shares). Default true; false = download-only. */
159
165
  sharePreview?: boolean;
166
+ /** Opt-in per-event analytics on a share link (visitor IP/user-agent persisted
167
+ * to `_fluxfiles/share-events/<jti>.jsonl`) — kept separate from `allowShare`
168
+ * since it's a real privacy footprint. Default false. */
169
+ shareAnalytics?: boolean;
160
170
  allowIntake?: boolean;
161
171
  /** Public base the intake create response builds the portal link from
162
172
  * (e.g. `https://files.acme.com/public/intake.html`). http(s) only; empty = the
package/dist/index.d.ts CHANGED
@@ -143,6 +143,12 @@ interface BaseTokenOptions {
143
143
  /** Edition preset that defaults a tier's claims (`pro`/`agency`/`enterprise`).
144
144
  * DX sugar — explicit claim options still win, and the license gates the code. */
145
145
  edition?: 'free' | 'pro' | 'agency' | 'enterprise';
146
+ /** Role preset that defaults a person's capability level (perms, owner-scoping,
147
+ * and free power-user toggles) — orthogonal to `edition`, which defaults which
148
+ * *paid features* a tier gets. DX sugar — explicit claim options still win, and
149
+ * `role` never itself becomes a JWT claim. See docs/ACL-ROLE-PRESETS-DESIGN.md.
150
+ * Not supported on BYOB tokens (matches `edition`'s existing BYOB exclusion). */
151
+ role?: 'viewer' | 'editor' | 'admin' | 'superadmin';
146
152
  /** Paid-module claims (3-layer gate: code installed + licensed + this claim).
147
153
  * All default off; inert unless the module is installed & licensed on the server. */
148
154
  allowShare?: boolean;
@@ -157,6 +163,10 @@ interface BaseTokenOptions {
157
163
  /** Allow the share landing to render an inline preview (images via /api/fm/img;
158
164
  * PDFs only on uncapped shares). Default true; false = download-only. */
159
165
  sharePreview?: boolean;
166
+ /** Opt-in per-event analytics on a share link (visitor IP/user-agent persisted
167
+ * to `_fluxfiles/share-events/<jti>.jsonl`) — kept separate from `allowShare`
168
+ * since it's a real privacy footprint. Default false. */
169
+ shareAnalytics?: boolean;
160
170
  allowIntake?: boolean;
161
171
  /** Public base the intake create response builds the portal link from
162
172
  * (e.g. `https://files.acme.com/public/intake.html`). http(s) only; empty = the
package/dist/index.js CHANGED
@@ -84,12 +84,13 @@ function newJti() {
84
84
  function createToken(opts) {
85
85
  const secret = resolveSecret(opts.secret);
86
86
  const now = Math.floor(Date.now() / 1e3);
87
+ const rolePreset = opts.role ? ROLE_PRESETS[String(opts.role).toLowerCase()] : void 0;
87
88
  const payload = {
88
89
  sub: opts.userId,
89
90
  iat: now,
90
91
  exp: now + (opts.ttl ?? 3600),
91
92
  jti: newJti(),
92
- perms: opts.perms ?? ["read"],
93
+ perms: opts.perms ?? (rolePreset == null ? void 0 : rolePreset.perms) ?? ["read"],
93
94
  disks: opts.disks ?? ["local"],
94
95
  prefix: opts.prefix ?? "",
95
96
  max_upload: opts.maxUploadMb ?? 10,
@@ -97,8 +98,8 @@ function createToken(opts) {
97
98
  max_storage: opts.maxStorageMb ?? 0,
98
99
  max_files: opts.maxFiles ?? 0
99
100
  };
100
- if (opts.ownerOnly) payload.owner_only = true;
101
- applyTenantOverrides(payload, opts);
101
+ if (opts.ownerOnly ?? (rolePreset == null ? void 0 : rolePreset.owner_only)) payload.owner_only = true;
102
+ applyTenantOverrides(payload, opts, rolePreset);
102
103
  return sign(payload, secret);
103
104
  }
104
105
  function createByobToken(opts) {
@@ -139,13 +140,61 @@ function sanitizeVariants(v) {
139
140
  var EDITION_PRESETS = {
140
141
  pro: { allow_optimize: true, allow_share: true, allow_intake: true },
141
142
  agency: { allow_optimize: true, allow_share: true, allow_intake: true },
142
- enterprise: { allow_optimize: true, allow_share: true, allow_intake: true, allow_virus_scan: true, allow_c2pa: true }
143
+ studio: {
144
+ allow_optimize: true,
145
+ allow_share: true,
146
+ allow_intake: true,
147
+ allow_versioning: true,
148
+ allow_webhooks: true,
149
+ allow_ai_vision: true,
150
+ allow_ocr: true
151
+ },
152
+ enterprise: {
153
+ allow_optimize: true,
154
+ allow_share: true,
155
+ allow_intake: true,
156
+ allow_versioning: true,
157
+ allow_webhooks: true,
158
+ allow_ai_vision: true,
159
+ allow_ocr: true,
160
+ allow_virus_scan: true,
161
+ allow_c2pa: true,
162
+ allow_backup: true,
163
+ allow_audit_export: true,
164
+ allow_dlp_scan: true,
165
+ allow_legal_hold: true
166
+ }
143
167
  };
144
- function applyTenantOverrides(payload, opts) {
168
+ var ROLE_PRESETS = {
169
+ viewer: { perms: ["read"], owner_only: true, allow_extract: false, allow_chmod: false },
170
+ editor: { perms: ["read", "write"], owner_only: true, allow_extract: true, allow_chmod: false },
171
+ admin: {
172
+ perms: ["read", "write", "delete", "audit"],
173
+ owner_only: false,
174
+ allow_extract: true,
175
+ allow_chmod: true,
176
+ allow_code_edit: true,
177
+ show_hidden: true
178
+ },
179
+ superadmin: {
180
+ perms: ["read", "write", "delete", "audit"],
181
+ owner_only: false,
182
+ allow_extract: true,
183
+ allow_chmod: true,
184
+ allow_code_edit: true,
185
+ show_hidden: true
186
+ }
187
+ };
188
+ function applyTenantOverrides(payload, opts, rolePreset) {
145
189
  const preset = opts.edition ? EDITION_PRESETS[String(opts.edition).toLowerCase()] : void 0;
146
190
  if (preset) {
147
191
  for (const [k, v] of Object.entries(preset)) if (!(k in payload)) payload[k] = v;
148
192
  }
193
+ if (rolePreset) {
194
+ for (const [k, v] of Object.entries(rolePreset)) {
195
+ if (k !== "perms" && k !== "owner_only" && !(k in payload)) payload[k] = v;
196
+ }
197
+ }
149
198
  if (opts.aiAutoTag !== void 0) payload.ai_auto_tag = !!opts.aiAutoTag;
150
199
  if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
151
200
  if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
@@ -181,6 +230,7 @@ function applyTenantOverrides(payload, opts) {
181
230
  if (opts.shareUrlTtl && opts.shareUrlTtl > 0) payload.share_url_ttl = Math.trunc(opts.shareUrlTtl);
182
231
  if (opts.shareBaseUrl) payload.share_base_url = String(opts.shareBaseUrl);
183
232
  if (opts.sharePreview !== void 0) payload.share_preview = !!opts.sharePreview;
233
+ if (opts.shareAnalytics !== void 0) payload.share_analytics = !!opts.shareAnalytics;
184
234
  if (opts.allowIntake !== void 0) payload.allow_intake = !!opts.allowIntake;
185
235
  if (opts.intakeBaseUrl) payload.intake_base_url = String(opts.intakeBaseUrl);
186
236
  if (opts.allowVersioning !== void 0) payload.allow_versioning = !!opts.allowVersioning;
package/dist/index.mjs CHANGED
@@ -62,12 +62,13 @@ function newJti() {
62
62
  function createToken(opts) {
63
63
  const secret = resolveSecret(opts.secret);
64
64
  const now = Math.floor(Date.now() / 1e3);
65
+ const rolePreset = opts.role ? ROLE_PRESETS[String(opts.role).toLowerCase()] : void 0;
65
66
  const payload = {
66
67
  sub: opts.userId,
67
68
  iat: now,
68
69
  exp: now + (opts.ttl ?? 3600),
69
70
  jti: newJti(),
70
- perms: opts.perms ?? ["read"],
71
+ perms: opts.perms ?? (rolePreset == null ? void 0 : rolePreset.perms) ?? ["read"],
71
72
  disks: opts.disks ?? ["local"],
72
73
  prefix: opts.prefix ?? "",
73
74
  max_upload: opts.maxUploadMb ?? 10,
@@ -75,8 +76,8 @@ function createToken(opts) {
75
76
  max_storage: opts.maxStorageMb ?? 0,
76
77
  max_files: opts.maxFiles ?? 0
77
78
  };
78
- if (opts.ownerOnly) payload.owner_only = true;
79
- applyTenantOverrides(payload, opts);
79
+ if (opts.ownerOnly ?? (rolePreset == null ? void 0 : rolePreset.owner_only)) payload.owner_only = true;
80
+ applyTenantOverrides(payload, opts, rolePreset);
80
81
  return sign(payload, secret);
81
82
  }
82
83
  function createByobToken(opts) {
@@ -117,13 +118,61 @@ function sanitizeVariants(v) {
117
118
  var EDITION_PRESETS = {
118
119
  pro: { allow_optimize: true, allow_share: true, allow_intake: true },
119
120
  agency: { allow_optimize: true, allow_share: true, allow_intake: true },
120
- enterprise: { allow_optimize: true, allow_share: true, allow_intake: true, allow_virus_scan: true, allow_c2pa: true }
121
+ studio: {
122
+ allow_optimize: true,
123
+ allow_share: true,
124
+ allow_intake: true,
125
+ allow_versioning: true,
126
+ allow_webhooks: true,
127
+ allow_ai_vision: true,
128
+ allow_ocr: true
129
+ },
130
+ enterprise: {
131
+ allow_optimize: true,
132
+ allow_share: true,
133
+ allow_intake: true,
134
+ allow_versioning: true,
135
+ allow_webhooks: true,
136
+ allow_ai_vision: true,
137
+ allow_ocr: true,
138
+ allow_virus_scan: true,
139
+ allow_c2pa: true,
140
+ allow_backup: true,
141
+ allow_audit_export: true,
142
+ allow_dlp_scan: true,
143
+ allow_legal_hold: true
144
+ }
121
145
  };
122
- function applyTenantOverrides(payload, opts) {
146
+ var ROLE_PRESETS = {
147
+ viewer: { perms: ["read"], owner_only: true, allow_extract: false, allow_chmod: false },
148
+ editor: { perms: ["read", "write"], owner_only: true, allow_extract: true, allow_chmod: false },
149
+ admin: {
150
+ perms: ["read", "write", "delete", "audit"],
151
+ owner_only: false,
152
+ allow_extract: true,
153
+ allow_chmod: true,
154
+ allow_code_edit: true,
155
+ show_hidden: true
156
+ },
157
+ superadmin: {
158
+ perms: ["read", "write", "delete", "audit"],
159
+ owner_only: false,
160
+ allow_extract: true,
161
+ allow_chmod: true,
162
+ allow_code_edit: true,
163
+ show_hidden: true
164
+ }
165
+ };
166
+ function applyTenantOverrides(payload, opts, rolePreset) {
123
167
  const preset = opts.edition ? EDITION_PRESETS[String(opts.edition).toLowerCase()] : void 0;
124
168
  if (preset) {
125
169
  for (const [k, v] of Object.entries(preset)) if (!(k in payload)) payload[k] = v;
126
170
  }
171
+ if (rolePreset) {
172
+ for (const [k, v] of Object.entries(rolePreset)) {
173
+ if (k !== "perms" && k !== "owner_only" && !(k in payload)) payload[k] = v;
174
+ }
175
+ }
127
176
  if (opts.aiAutoTag !== void 0) payload.ai_auto_tag = !!opts.aiAutoTag;
128
177
  if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
129
178
  if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
@@ -159,6 +208,7 @@ function applyTenantOverrides(payload, opts) {
159
208
  if (opts.shareUrlTtl && opts.shareUrlTtl > 0) payload.share_url_ttl = Math.trunc(opts.shareUrlTtl);
160
209
  if (opts.shareBaseUrl) payload.share_base_url = String(opts.shareBaseUrl);
161
210
  if (opts.sharePreview !== void 0) payload.share_preview = !!opts.sharePreview;
211
+ if (opts.shareAnalytics !== void 0) payload.share_analytics = !!opts.shareAnalytics;
162
212
  if (opts.allowIntake !== void 0) payload.allow_intake = !!opts.allowIntake;
163
213
  if (opts.intakeBaseUrl) payload.intake_base_url = String(opts.intakeBaseUrl);
164
214
  if (opts.allowVersioning !== void 0) payload.allow_versioning = !!opts.allowVersioning;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxfiles/node",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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,
package/src/token.ts CHANGED
@@ -34,12 +34,16 @@ function newJti(): string {
34
34
  export function createToken(opts: CreateTokenOptions): string {
35
35
  const secret = resolveSecret(opts.secret);
36
36
  const now = Math.floor(Date.now() / 1000);
37
+ // Role preset (DX sugar, docs/ACL-ROLE-PRESETS-DESIGN.md): resolved BEFORE the base
38
+ // payload object, because `perms` already has an unconditional default baked into
39
+ // that object below — a plain "set if absent" guard would never fire for it.
40
+ const rolePreset = opts.role ? ROLE_PRESETS[String(opts.role).toLowerCase()] : undefined;
37
41
  const payload: Record<string, unknown> = {
38
42
  sub: opts.userId,
39
43
  iat: now,
40
44
  exp: now + (opts.ttl ?? 3600),
41
45
  jti: newJti(),
42
- perms: opts.perms ?? ['read'],
46
+ perms: opts.perms ?? (rolePreset?.perms as string[] | undefined) ?? ['read'],
43
47
  disks: opts.disks ?? ['local'],
44
48
  prefix: opts.prefix ?? '',
45
49
  max_upload: opts.maxUploadMb ?? 10,
@@ -47,8 +51,8 @@ export function createToken(opts: CreateTokenOptions): string {
47
51
  max_storage: opts.maxStorageMb ?? 0,
48
52
  max_files: opts.maxFiles ?? 0,
49
53
  };
50
- if (opts.ownerOnly) payload.owner_only = true;
51
- applyTenantOverrides(payload, opts);
54
+ if (opts.ownerOnly ?? (rolePreset?.owner_only as boolean | undefined)) payload.owner_only = true;
55
+ applyTenantOverrides(payload, opts, rolePreset);
52
56
  return sign(payload, secret);
53
57
  }
54
58
 
@@ -101,13 +105,49 @@ function sanitizeVariants(v: BaseTokenOptions['variants']): Record<string, numbe
101
105
  const EDITION_PRESETS: Record<string, Record<string, boolean>> = {
102
106
  pro: { allow_optimize: true, allow_share: true, allow_intake: true },
103
107
  agency: { allow_optimize: true, allow_share: true, allow_intake: true },
104
- enterprise: { allow_optimize: true, allow_share: true, allow_intake: true, allow_virus_scan: true, allow_c2pa: true },
108
+ studio: {
109
+ allow_optimize: true, allow_share: true, allow_intake: true,
110
+ allow_versioning: true, allow_webhooks: true, allow_ai_vision: true, allow_ocr: true,
111
+ },
112
+ enterprise: {
113
+ allow_optimize: true, allow_share: true, allow_intake: true,
114
+ allow_versioning: true, allow_webhooks: true, allow_ai_vision: true, allow_ocr: true,
115
+ allow_virus_scan: true, allow_c2pa: true, allow_backup: true, allow_audit_export: true,
116
+ allow_dlp_scan: true, allow_legal_hold: true,
117
+ },
118
+ };
119
+
120
+ /** Role preset (DX sugar, docs/ACL-ROLE-PRESETS-DESIGN.md): default a person's
121
+ * capability level; explicit opts always win. `role` never itself becomes a JWT
122
+ * claim — it only ever expands into ordinary claims decoded server-side already. */
123
+ const ROLE_PRESETS: Record<string, Record<string, unknown>> = {
124
+ viewer: { perms: ['read'], owner_only: true, allow_extract: false, allow_chmod: false },
125
+ editor: { perms: ['read', 'write'], owner_only: true, allow_extract: true, allow_chmod: false },
126
+ admin: {
127
+ perms: ['read', 'write', 'delete', 'audit'], owner_only: false,
128
+ allow_extract: true, allow_chmod: true, allow_code_edit: true, show_hidden: true,
129
+ },
130
+ superadmin: {
131
+ perms: ['read', 'write', 'delete', 'audit'], owner_only: false,
132
+ allow_extract: true, allow_chmod: true, allow_code_edit: true, show_hidden: true,
133
+ },
105
134
  };
106
135
 
107
136
  /** Copy the optional per-tenant override claims into a payload when set. */
108
- function applyTenantOverrides(payload: Record<string, unknown>, opts: BaseTokenOptions): void {
137
+ function applyTenantOverrides(
138
+ payload: Record<string, unknown>,
139
+ opts: BaseTokenOptions,
140
+ rolePreset?: Record<string, unknown>,
141
+ ): void {
109
142
  const preset = opts.edition ? EDITION_PRESETS[String(opts.edition).toLowerCase()] : undefined;
110
143
  if (preset) for (const [k, v] of Object.entries(preset)) if (!(k in payload)) payload[k] = v;
144
+ // Role preset: the rest of the bundle, excluding `perms`/`owner_only` — both are
145
+ // already resolved above (in createToken()), before this function ever runs.
146
+ if (rolePreset) {
147
+ for (const [k, v] of Object.entries(rolePreset)) {
148
+ if (k !== 'perms' && k !== 'owner_only' && !(k in payload)) payload[k] = v;
149
+ }
150
+ }
111
151
  if (opts.aiAutoTag !== undefined) payload.ai_auto_tag = !!opts.aiAutoTag;
112
152
  if (opts.rateRead && opts.rateRead > 0) payload.rate_read = Math.trunc(opts.rateRead);
113
153
  if (opts.rateWrite && opts.rateWrite > 0) payload.rate_write = Math.trunc(opts.rateWrite);
@@ -156,6 +196,7 @@ function applyTenantOverrides(payload: Record<string, unknown>, opts: BaseTokenO
156
196
  if (opts.shareUrlTtl && opts.shareUrlTtl > 0) payload.share_url_ttl = Math.trunc(opts.shareUrlTtl);
157
197
  if (opts.shareBaseUrl) payload.share_base_url = String(opts.shareBaseUrl);
158
198
  if (opts.sharePreview !== undefined) payload.share_preview = !!opts.sharePreview;
199
+ if (opts.shareAnalytics !== undefined) payload.share_analytics = !!opts.shareAnalytics;
159
200
  if (opts.allowIntake !== undefined) payload.allow_intake = !!opts.allowIntake;
160
201
  // Intake portal link base — same shape as shareBaseUrl (the core drops a
161
202
  // non-http(s) value on decode).
package/src/types.ts CHANGED
@@ -147,6 +147,12 @@ export interface BaseTokenOptions {
147
147
  /** Edition preset that defaults a tier's claims (`pro`/`agency`/`enterprise`).
148
148
  * DX sugar — explicit claim options still win, and the license gates the code. */
149
149
  edition?: 'free' | 'pro' | 'agency' | 'enterprise';
150
+ /** Role preset that defaults a person's capability level (perms, owner-scoping,
151
+ * and free power-user toggles) — orthogonal to `edition`, which defaults which
152
+ * *paid features* a tier gets. DX sugar — explicit claim options still win, and
153
+ * `role` never itself becomes a JWT claim. See docs/ACL-ROLE-PRESETS-DESIGN.md.
154
+ * Not supported on BYOB tokens (matches `edition`'s existing BYOB exclusion). */
155
+ role?: 'viewer' | 'editor' | 'admin' | 'superadmin';
150
156
  /** Paid-module claims (3-layer gate: code installed + licensed + this claim).
151
157
  * All default off; inert unless the module is installed & licensed on the server. */
152
158
  allowShare?: boolean;
@@ -161,6 +167,10 @@ export interface BaseTokenOptions {
161
167
  /** Allow the share landing to render an inline preview (images via /api/fm/img;
162
168
  * PDFs only on uncapped shares). Default true; false = download-only. */
163
169
  sharePreview?: boolean;
170
+ /** Opt-in per-event analytics on a share link (visitor IP/user-agent persisted
171
+ * to `_fluxfiles/share-events/<jti>.jsonl`) — kept separate from `allowShare`
172
+ * since it's a real privacy footprint. Default false. */
173
+ shareAnalytics?: boolean;
164
174
  allowIntake?: boolean;
165
175
  /** Public base the intake create response builds the portal link from
166
176
  * (e.g. `https://files.acme.com/public/intake.html`). http(s) only; empty = the