@arcote.tech/arc-files 0.7.26 → 0.7.28

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-files",
3
3
  "type": "module",
4
- "version": "0.7.26",
4
+ "version": "0.7.28",
5
5
  "private": false,
6
6
  "description": "Generic file upload fragment for Arc framework — S3 presigned upload + provider file_id binding",
7
7
  "main": "./src/index.ts",
@@ -10,9 +10,9 @@
10
10
  "type-check": "tsc --noEmit"
11
11
  },
12
12
  "peerDependencies": {
13
- "@arcote.tech/arc": "^0.7.26",
14
- "@arcote.tech/arc-ds": "^0.7.26",
15
- "@arcote.tech/platform": "^0.7.26",
13
+ "@arcote.tech/arc": "^0.7.28",
14
+ "@arcote.tech/arc-ds": "^0.7.28",
15
+ "@arcote.tech/platform": "^0.7.28",
16
16
  "@aws-sdk/client-s3": "^3.658.0",
17
17
  "@aws-sdk/s3-request-presigner": "^3.658.0",
18
18
  "lucide-react": ">=0.400.0",
@@ -8,6 +8,7 @@ import {
8
8
  string,
9
9
  type ArcAggregateElement,
10
10
  type ArcId,
11
+ type ArcRawShape,
11
12
  } from "@arcote.tech/arc";
12
13
  import type { S3Bridge } from "../s3/s3-client";
13
14
  import type { FileProviderName } from "../types";
@@ -51,9 +52,20 @@ export function createFileTypeAggregate<const Data extends FileTypeData>(
51
52
  const metadataShape = data.metadata;
52
53
  const fullShape = mergeUnsafe(builtInFields, metadataShape);
53
54
 
54
- // `requestUpload` params zawiera built-in (name, mime, size) + WSZYSTKIE
55
- // custom metadata fields consumer dostarcza je przy uploadzie (np.
56
- // accountId, opcjonalnie workspaceId).
55
+ // `requestUpload` params = built-in (name, mime, size) + custom metadata,
56
+ // ale metadata OPCJONALNE. Pola scope (accountId itd.) wyprowadzane
57
+ // autorytatywnie z aktywnego tokena server-side (patrz handler niżej), więc
58
+ // klient nie musi — i nie powinien — ich wysyłać. Consumer może pominąć
59
+ // wartość pochodzącą z tokena; serwer i tak ją nadpisze wartością z tokena.
60
+ const optionalMetadata: ArcRawShape = {};
61
+ for (const [key, element] of Object.entries(metadataShape)) {
62
+ // `validate(undefined) === false` → element już jest opcjonalny
63
+ // (ArcOptional), nie owijaj ponownie (ArcOptional nie ma `.optional()`).
64
+ const alreadyOptional = (element as any).validate?.(undefined) === false;
65
+ optionalMetadata[key] = alreadyOptional
66
+ ? element
67
+ : (element as any).optional();
68
+ }
57
69
  const requestUploadParams = mergeUnsafe(
58
70
  {
59
71
  _id: fileId,
@@ -61,7 +73,7 @@ export function createFileTypeAggregate<const Data extends FileTypeData>(
61
73
  mime: string(),
62
74
  size: number(),
63
75
  },
64
- metadataShape,
76
+ optionalMetadata,
65
77
  );
66
78
 
67
79
  // `fileRequested` event payload = `requestUploadParams` + s3 fields
@@ -142,6 +154,27 @@ export function createFileTypeAggregate<const Data extends FileTypeData>(
142
154
  const s3Key = `${data.name}/${p._id}/${p.name}`;
143
155
  const uploadUrl = await s3.presignUpload(s3Key, p.name && p.mime);
144
156
 
157
+ // Pola scope (accountId itd.) bierzemy AUTORYTATYWNIE z aktywnego
158
+ // tokena — nigdy z params klienta. `protection.check(tokenPayload)`
159
+ // zwraca dokładnie mapowanie token→metadata (`{accountId: p.accountId}`),
160
+ // to samo, którego arc-core używa do restrykcji scope. Zapobiega
161
+ // podszyciu pod cudze accountId i znosi wymóg wysyłania go przez
162
+ // klienta (który przed załadowaniem `me` słał undefined → padała
163
+ // walidacja wejścia).
164
+ const auth = (ctx as any).$auth as
165
+ | { params: any; tokenName: string }
166
+ | undefined;
167
+ const derived: Record<string, any> = {};
168
+ if (auth?.tokenName) {
169
+ for (const protection of data.protections) {
170
+ if ((protection.token as any).name !== auth.tokenName) continue;
171
+ const restriction = protection.check(auth.params);
172
+ if (restriction && typeof restriction === "object") {
173
+ Object.assign(derived, restriction);
174
+ }
175
+ }
176
+ }
177
+
145
178
  const payload: Record<string, any> = {
146
179
  _id: p._id,
147
180
  name: p.name,
@@ -151,7 +184,7 @@ export function createFileTypeAggregate<const Data extends FileTypeData>(
151
184
  s3Key,
152
185
  };
153
186
  for (const key of Object.keys(metadataShape)) {
154
- payload[key] = p[key];
187
+ payload[key] = key in derived ? derived[key] : p[key];
155
188
  }
156
189
  await (ctx as any).fileRequested.emit(payload);
157
190
 
@@ -2,8 +2,10 @@
2
2
  import type {
3
3
  ArcId,
4
4
  ArcRawShape,
5
+ ArcRawShapeType,
5
6
  ArcTokenAny,
6
7
  Merge,
8
+ ViewProtectionFn,
7
9
  } from "@arcote.tech/arc";
8
10
  import { createFileTypeAggregate } from "./aggregates/file";
9
11
  import type { S3Bridge } from "./s3/s3-client";
@@ -17,9 +19,24 @@ export interface FileTypeProtection {
17
19
  token: ArcTokenAny;
18
20
  // `params` is the decoded token payload; user-provided check returns
19
21
  // a where-clause restriction (`{field: value}`) or `false` to deny.
20
- check: (params: any) => Record<string, unknown> | false;
22
+ // Typowane tym samym `ViewProtectionFn` co `aggregate.protectBy` — payload
23
+ // tokena inferowany z `token` (zero `any` po stronie konsumenta).
24
+ check: ViewProtectionFn;
21
25
  }
22
26
 
27
+ /**
28
+ * Ctx handlera custom mutation (`.withMutation()`). Auto-generowany emitter
29
+ * `<name>Performed` jest TYPOWANY (łapie literówkę w nazwie eventu). Reszta
30
+ * ctx (`modify`/`$query`/`set`/`findOne`) pozostaje permisywna — pełne
31
+ * otypowanie blokuje limit instancjacji TS na rosnących tuple
32
+ * Events/MutateMethods (ten sam powód co `aggAny: any` w `aggregates/file.ts`).
33
+ */
34
+ export type FileMutationContext<N extends string> = {
35
+ [K in `${N}Performed`]: {
36
+ emit: (payload: Record<string, unknown>) => Promise<void>;
37
+ };
38
+ } & Record<string, any>;
39
+
23
40
  /**
24
41
  * Custom mutation rejestrowana przez `.withMutation()`. Builder generuje
25
42
  * dedicated event `<name>Performed` + mutation method z params. Handler
@@ -95,10 +112,7 @@ export class ArcFileTypeBuilder<
95
112
  * tokena. Każdy `.protectedBy()` dodaje jeden entry do listy; przy
96
113
  * `.build()` builder iteruje i woła `aggregate.protectBy()` per entry.
97
114
  */
98
- protectedBy<T extends ArcTokenAny>(
99
- token: T,
100
- check: (params: any) => Record<string, unknown> | false,
101
- ) {
115
+ protectedBy<T extends ArcTokenAny>(token: T, check: ViewProtectionFn<T>) {
102
116
  type NewProtections = [
103
117
  ...Data["protections"],
104
118
  { token: T; check: typeof check },
@@ -126,7 +140,10 @@ export class ArcFileTypeBuilder<
126
140
  withMutation<const N extends string, const P extends ArcRawShape>(
127
141
  name: N,
128
142
  params: P,
129
- handler: (ctx: any, params: any) => Promise<void>,
143
+ handler: (
144
+ ctx: FileMutationContext<N>,
145
+ params: { _id: string } & ArcRawShapeType<P>,
146
+ ) => Promise<void>,
130
147
  ) {
131
148
  type NewMutations = [
132
149
  ...Data["mutations"],