wcz-layout 9.4.1 → 9.4.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/dist/components.mjs +113 -336
- package/dist/components.mjs.map +1 -1
- package/dist/data/client.d.mts +8 -2
- package/dist/data/client.mjs +1 -1
- package/dist/data/server.d.mts +1 -1
- package/dist/data/server.mjs +1 -1
- package/dist/{file-DM1x40eJ.mjs → file-Be2JHq7h.mjs} +17 -30
- package/dist/file-Be2JHq7h.mjs.map +1 -0
- package/dist/{file-C5-a_6W6.mjs → file-bckp9fpa.mjs} +24 -21
- package/dist/file-bckp9fpa.mjs.map +1 -0
- package/dist/hooks.mjs +4 -4
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.mjs +9 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{peoplesoft-gKd-Jh5L.d.mts → peoplesoft-MZwYDjPB.d.mts} +4 -5
- package/package.json +8 -8
- package/skills/client-db/SKILL.md +10 -17
- package/skills/dialogs/SKILL.md +1 -1
- package/skills/notifications/SKILL.md +3 -3
- package/skills/routing/SKILL.md +9 -5
- package/skills/server-functions/SKILL.md +12 -13
- package/skills/services/SKILL.md +4 -2
- package/skills/services/docs/approval.md +4 -3
- package/skills/services/docs/file.md +35 -10
- package/skills/start/steps/01-git-setup.md +1 -1
- package/skills/start/steps/04-database-setup.md +5 -1
- package/skills/start/steps/05-entra-setup.md +3 -3
- package/skills/start/steps/06-vault-setup.md +50 -29
- package/dist/file-C5-a_6W6.mjs.map +0 -1
- package/dist/file-DM1x40eJ.mjs.map +0 -1
|
@@ -3,13 +3,14 @@ name: client-db
|
|
|
3
3
|
description: "Use when: creating or modifying TanStack DB collections, live queries, optimistic collection writes, subset loading, collection preloading."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> Mechanics (`createCollection`, `queryCollectionOptions`, the live-query builder, `createOptimisticAction`) are covered by TanStack's own skills — load `@tanstack/db#db-core/live-queries`, `#db-core/mutations-optimistic`, and `@tanstack/react-db` for the API details. This skill adds only the wcz-layout conventions on top.
|
|
7
|
+
|
|
6
8
|
## Rules
|
|
7
9
|
|
|
8
10
|
- Use `eager` syncMode for top-level collections, `on-demand` for child/relational data.
|
|
9
11
|
- Use `useLiveQuery` for list/grid pages and `useLiveSuspenseQuery` for detail components wrapped in suspense.
|
|
10
12
|
- For mutations use `createOptimisticAction` — mirror the server mutation in `onMutate`, then call the server function and `collection.utils.refetch()` in `mutationFn`.
|
|
11
|
-
-
|
|
12
|
-
- Use Axios with auth interceptor `getAccessToken` but only for public REST APIs; use default `api` as a scope key.
|
|
13
|
+
- For on-demand subset loading, convert `meta.loadSubsetOptions` with `fromTanDb()` (from `agnostic-query/tanstack-db`) before passing it to the server fn, and set `autoIndex: "eager"` with `defaultIndexType: BasicIndex` (from `@tanstack/react-db`).
|
|
13
14
|
|
|
14
15
|
## File Placement
|
|
15
16
|
|
|
@@ -18,24 +19,11 @@ src/db-collections/ — DB collections
|
|
|
18
19
|
src/server/actions/ — server functions (select/update/insert/delete)
|
|
19
20
|
src/lib/schemas/ — Zod schemas (shared between client and server)
|
|
20
21
|
src/lib/auth/scopes.ts — API scope keys
|
|
21
|
-
wcz-layout/data — queryClient instance
|
|
22
|
-
wcz-layout/auth — getAccessToken utility function
|
|
23
22
|
```
|
|
24
23
|
|
|
25
24
|
## Examples
|
|
26
25
|
|
|
27
26
|
```ts
|
|
28
|
-
// src/db-collections/<feature>.ts
|
|
29
|
-
export const api = axios.create({
|
|
30
|
-
baseURL: "/api/libraries",
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
api.interceptors.request.use(async (config) => {
|
|
34
|
-
const accessToken = await getAccessToken("api");
|
|
35
|
-
config.headers.set("Authorization", `Bearer ${accessToken}`);
|
|
36
|
-
return config;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
27
|
// src/db-collections/<feature>.ts
|
|
40
28
|
export const librariesCollection = createCollection(
|
|
41
29
|
queryCollectionOptions({
|
|
@@ -48,15 +36,20 @@ export const librariesCollection = createCollection(
|
|
|
48
36
|
}),
|
|
49
37
|
);
|
|
50
38
|
|
|
51
|
-
// src/db-collections/<feature>.ts
|
|
39
|
+
// src/db-collections/<feature>.ts — child/relational (on-demand)
|
|
40
|
+
import { BasicIndex } from "@tanstack/react-db";
|
|
41
|
+
import { fromTanDb } from "agnostic-query/tanstack-db";
|
|
42
|
+
|
|
52
43
|
export const bookCollection = createCollection(
|
|
53
44
|
queryCollectionOptions({
|
|
54
45
|
queryKey: ["books"],
|
|
55
|
-
queryFn: ({ meta }) => selectBooks({ data: meta?.loadSubsetOptions }),
|
|
46
|
+
queryFn: ({ meta }) => selectBooks({ data: fromTanDb(meta?.loadSubsetOptions) }),
|
|
56
47
|
getKey: ({ id }) => id,
|
|
57
48
|
schema: BookSchema,
|
|
58
49
|
queryClient: queryClient,
|
|
59
50
|
syncMode: "on-demand",
|
|
51
|
+
autoIndex: "eager",
|
|
52
|
+
defaultIndexType: BasicIndex,
|
|
60
53
|
}),
|
|
61
54
|
);
|
|
62
55
|
|
package/skills/dialogs/SKILL.md
CHANGED
|
@@ -7,9 +7,9 @@ description: "Use when: displaying success, error, warning, or informational fee
|
|
|
7
7
|
|
|
8
8
|
- Use `useNotification()` for lightweight non-blocking user feedback.
|
|
9
9
|
- Always use translation; add new keys for feature-specific messages.
|
|
10
|
-
- Notifications are automatically queued and
|
|
11
|
-
-
|
|
12
|
-
- Use `severity: "success" | "info" | "warning" | "error"` consistently with the outcome.
|
|
10
|
+
- Notifications are automatically queued by the provider and shown one at a time.
|
|
11
|
+
- `autoHideDuration` defaults to 5000 ms and is not capped by the library; keep overrides reasonable (~10000 ms) for messages that need more reading time.
|
|
12
|
+
- Use `severity: "success" | "info" | "warning" | "error"` consistently with the outcome. Severity is optional — omitting it renders a plain snackbar (no colored `Alert`).
|
|
13
13
|
- Use notifications for lightweight feedback; dialogs when the user must decide or acknowledge.
|
|
14
14
|
|
|
15
15
|
## File Placement
|
package/skills/routing/SKILL.md
CHANGED
|
@@ -3,9 +3,11 @@ name: routing
|
|
|
3
3
|
description: "Use when: creating or modifying TanStack Router routes, feature folders, navigation, permissions, loaders, suspense flows, or route-scoped components/hooks."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> Mechanics (`createFileRoute`, `beforeLoad`, `redirect`, `loader`, preloading, `pendingComponent`) are covered by TanStack's own skills — load `@tanstack/router-core#router-core/auth-and-guards`, `#router-core/data-loading`, and `#router-core/navigation` for the API details. This skill adds only the wcz-layout conventions on top.
|
|
7
|
+
|
|
6
8
|
## Rules
|
|
7
9
|
|
|
8
|
-
-
|
|
10
|
+
- Include the `requirePermission(<permissionKey>)` function in the route's `beforeLoad` to enforce access control — pass a key defined in your app's `src/lib/auth/permissions.ts`; omit it only for intentionally public routes.
|
|
9
11
|
- Use `Route.useRouteContext()` to access the authenticated user. User can be `null` if the route doesn't include `requirePermission`.
|
|
10
12
|
- Use the route `loader` option to preload db collections. Preload only root-level collections.
|
|
11
13
|
- Create a `pendingComponent` for routes that suspend or preload meaningful data.
|
|
@@ -15,6 +17,7 @@ description: "Use when: creating or modifying TanStack Router routes, feature fo
|
|
|
15
17
|
- Colocate route-specific components/hooks inside `routes/<feature>/-components` or `-hooks`.
|
|
16
18
|
- Root-level `components/` and `hooks/` are only for code shared across multiple routes.
|
|
17
19
|
- After creating a new route, add or ask for a navigation item with a unique icon from `@mui/icons-material` in `src/routes/__root.tsx`.
|
|
20
|
+
- For navigation and redirects, always use the type-safe router components from `wcz-layout/components` (`RouterButton`, `RouterIconButton`, `RouterLink`, `RouterFab`, `RouterTab`, `RouterListItemButton`, `RouterGridActionsCellItem`) rather than raw MUI with a manual `href` — they wrap `createLink`, so `to`/`params`/`search` are type-checked.
|
|
18
21
|
|
|
19
22
|
## Base Project Structure
|
|
20
23
|
|
|
@@ -64,7 +67,7 @@ import { requirePermission, uuidv7 } from "wcz-layout/utils";
|
|
|
64
67
|
// src/routes/features/create.tsx
|
|
65
68
|
export const Route = createFileRoute("/features/create")({
|
|
66
69
|
component: RouteComponent,
|
|
67
|
-
beforeLoad: requirePermission("
|
|
70
|
+
beforeLoad: requirePermission("admin"),
|
|
68
71
|
pendingComponent: FeatureSkeleton,
|
|
69
72
|
loader: () => {
|
|
70
73
|
featureCollection.preload();
|
|
@@ -119,10 +122,11 @@ const navigation: Navigation = [
|
|
|
119
122
|
},
|
|
120
123
|
];
|
|
121
124
|
|
|
122
|
-
// Routes are public
|
|
123
|
-
// its beforeLoad
|
|
125
|
+
// Routes are public unless guarded. By convention, protect every route by adding
|
|
126
|
+
// requirePermission to its beforeLoad (omit only for intentionally public routes) —
|
|
127
|
+
// it redirects to login when signed out, then checks the permission group.
|
|
124
128
|
export const Route = createFileRoute("/feature-name")({
|
|
125
129
|
component: RouteComponent,
|
|
126
|
-
beforeLoad: requirePermission("
|
|
130
|
+
beforeLoad: requirePermission("admin"),
|
|
127
131
|
});
|
|
128
132
|
```
|
|
@@ -3,13 +3,15 @@ name: server-functions
|
|
|
3
3
|
description: "Use when: creating or modifying TanStack Start server functions, REST routes, middleware, validation, database transactions, external service calls, or permission checks."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
> Mechanics (`createServerFn`, `.validator`, `createMiddleware`, `createHandlers`) are covered by TanStack's own skills — load `@tanstack/start-client-core#start-core/server-functions`, `#start-core/middleware`, and `#start-core/server-routes` for the API details. This skill adds only the wcz-layout middleware conventions on top.
|
|
7
|
+
|
|
6
8
|
## Rules
|
|
7
9
|
|
|
8
10
|
- Before generating code, clarify whether the feature should be exposed as public REST API routes or implemented as internal TanStack Start server functions (recommended), and what permission should be required for access.
|
|
9
11
|
- If permission is unknown, use `"all"`.
|
|
10
12
|
- Every server function MUST include `authorizationMiddleware` — server functions are callable RPC endpoints, so a route-level `requirePermission` in `beforeLoad` only hides the UI and does not protect them.
|
|
11
|
-
- Follow the chain order for server functions: `
|
|
12
|
-
-
|
|
13
|
+
- Follow the chain order for server functions: `middleware` → `validator` → `handler`.
|
|
14
|
+
- `authorizationMiddleware` MUST be the LAST middleware in the array so its `context.user` (a non-null `User`) is the value that reaches the handler — `databaseMiddleware` re-derives `user` as nullable, so putting it after would widen the type back to `User | null`. Order: `[databaseMiddleware, authorizationMiddleware(<key>)]`. In REST routes, `validationMiddleware` also comes before `authorizationMiddleware`.
|
|
13
15
|
- Use `validationMiddleware` only in REST API routes. Server functions use `.validator()`.
|
|
14
16
|
- `databaseMiddleware` wraps the handler in a DB transaction and sets `app.user_name` so audit triggers can record who made the change.
|
|
15
17
|
- Reuse schemas from `src/lib/schemas/`.
|
|
@@ -31,28 +33,28 @@ src/lib/auth/permissions.ts — permission keys
|
|
|
31
33
|
```ts
|
|
32
34
|
// src/server/actions/<feature>.ts
|
|
33
35
|
export const selectFeatures = createServerFn()
|
|
34
|
-
.middleware([authorizationMiddleware("all")
|
|
36
|
+
.middleware([databaseMiddleware, authorizationMiddleware("all")])
|
|
35
37
|
.handler(async ({ context }) => {
|
|
36
38
|
return await context.db.select().from(featureTable);
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
export const insertFeature = createServerFn({ method: "POST" })
|
|
42
|
+
.middleware([databaseMiddleware, authorizationMiddleware("admin")])
|
|
40
43
|
.validator(FeatureSchema)
|
|
41
|
-
.middleware([authorizationMiddleware("admin"), databaseMiddleware])
|
|
42
44
|
.handler(async ({ data, context }) => {
|
|
43
45
|
await context.db.insert(featureTable).values(data);
|
|
44
46
|
});
|
|
45
47
|
|
|
46
48
|
export const updateFeature = createServerFn({ method: "POST" })
|
|
49
|
+
.middleware([databaseMiddleware, authorizationMiddleware("admin")])
|
|
47
50
|
.validator(FeatureSchema)
|
|
48
|
-
.middleware([authorizationMiddleware("admin"), databaseMiddleware])
|
|
49
51
|
.handler(async ({ data, context }) => {
|
|
50
52
|
await context.db.update(featureTable).set(data).where(eq(featureTable.id, data.id));
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
export const deleteFeature = createServerFn({ method: "POST" })
|
|
56
|
+
.middleware([databaseMiddleware, authorizationMiddleware("admin")])
|
|
54
57
|
.validator(FeatureSchema.pick({ id: true }))
|
|
55
|
-
.middleware([authorizationMiddleware("admin"), databaseMiddleware])
|
|
56
58
|
.handler(async ({ data, context }) => {
|
|
57
59
|
await context.db.delete(featureTable).where(eq(featureTable.id, data.id));
|
|
58
60
|
});
|
|
@@ -60,21 +62,18 @@ export const deleteFeature = createServerFn({ method: "POST" })
|
|
|
60
62
|
// src/routes/api/<feature>s/index.ts
|
|
61
63
|
export const Route = createFileRoute("/api/features/")({
|
|
62
64
|
server: {
|
|
65
|
+
middleware: [databaseMiddleware],
|
|
63
66
|
handlers: ({ createHandlers }) =>
|
|
64
67
|
createHandlers({
|
|
65
68
|
GET: {
|
|
66
|
-
middleware: [authorizationMiddleware("all")
|
|
69
|
+
middleware: [authorizationMiddleware("all")],
|
|
67
70
|
handler: async ({ context }) => {
|
|
68
71
|
const items = await context.db.select().from(featureTable);
|
|
69
72
|
return Response.json(items);
|
|
70
73
|
},
|
|
71
74
|
},
|
|
72
75
|
POST: {
|
|
73
|
-
middleware: [
|
|
74
|
-
validationMiddleware(FeatureSchema),
|
|
75
|
-
authorizationMiddleware("admin"),
|
|
76
|
-
databaseMiddleware,
|
|
77
|
-
],
|
|
76
|
+
middleware: [validationMiddleware(FeatureSchema), authorizationMiddleware("admin")],
|
|
78
77
|
handler: async ({ context }) => {
|
|
79
78
|
const [response] = await context.db
|
|
80
79
|
.insert(featureTable)
|
|
@@ -90,7 +89,7 @@ export const Route = createFileRoute("/api/features/")({
|
|
|
90
89
|
// src/routes/api/<feature>s/$id.ts
|
|
91
90
|
export const Route = createFileRoute("/api/features/$id")({
|
|
92
91
|
server: {
|
|
93
|
-
middleware: [authorizationMiddleware("admin")
|
|
92
|
+
middleware: [databaseMiddleware, authorizationMiddleware("admin")],
|
|
94
93
|
handlers: ({ createHandlers }) =>
|
|
95
94
|
createHandlers({
|
|
96
95
|
PUT: {
|
package/skills/services/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: services
|
|
3
|
-
description: "Use when:
|
|
3
|
+
description: "Use when: working with file, approval, employee or email services into your workflow."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Info
|
|
@@ -9,7 +9,9 @@ You can read about each service in the links below. Services are categorized as
|
|
|
9
9
|
|
|
10
10
|
- **Client exports** — Contains `queryOptions` and `mutationOptions`; use for data fetching/mutations on the client with React Query.
|
|
11
11
|
- **Server exports** — Server functions; prefer calling mutations inside other server functions or database transactions.
|
|
12
|
-
-
|
|
12
|
+
- Invalidating mutation options take `{ queryClient }` so they can invalidate related queries; read-only ones (e.g. `downloadFileMutationOptions()`, `openFileMutationOptions()`) don't require it.
|
|
13
|
+
- Access `queryClient` with `Route.useRouteContext()` in a route file, or `useRouteContext({ from: "path" })` in a component.
|
|
14
|
+
- To read about a service, open the linked file in the **File** column directly.
|
|
13
15
|
|
|
14
16
|
## Import Paths
|
|
15
17
|
|
|
@@ -163,8 +163,8 @@ const ApprovalSchema = z.object({
|
|
|
163
163
|
createdBy: z.custom<ApprovalEmployee>(),
|
|
164
164
|
updated: z.date().optional(),
|
|
165
165
|
updatedBy: z.custom<ApprovalEmployee>().optional(),
|
|
166
|
-
approvalFlows: z.array(ApprovalFlow).min(1),
|
|
167
|
-
currentApprovers: z.array(ApprovalEmployee),
|
|
166
|
+
approvalFlows: z.array(z.custom<ApprovalFlow>()).min(1),
|
|
167
|
+
currentApprovers: z.array(z.custom<ApprovalEmployee>()),
|
|
168
168
|
coRequestors: z.array(ApprovalCoRequestorSchema).optional(),
|
|
169
169
|
});
|
|
170
170
|
|
|
@@ -245,11 +245,12 @@ const WithdrawApprovalSchema = z.object({
|
|
|
245
245
|
## Examples
|
|
246
246
|
|
|
247
247
|
```ts
|
|
248
|
-
import { approvalQueryOptions } from "wcz-layout/data/client";
|
|
248
|
+
import { approvalQueryOptions, approveApprovalMutationOptions } from "wcz-layout/data/client";
|
|
249
249
|
import { approveApproval } from "wcz-layout/data/server";
|
|
250
250
|
|
|
251
251
|
// client
|
|
252
252
|
const { data: approval } = useQuery(approvalQueryOptions({ id }));
|
|
253
|
+
const { mutate: approve } = useMutation(approveApprovalMutationOptions({ queryClient }));
|
|
253
254
|
|
|
254
255
|
// server
|
|
255
256
|
await approveApproval({ data: { id, result: "Approved", emailBody } });
|
|
@@ -44,11 +44,6 @@ const FileMetaSchema = z.object({
|
|
|
44
44
|
createdDate: z.date(),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
const FileActionsSchema = z.object({
|
|
48
|
-
download: z.boolean().optional(),
|
|
49
|
-
delete: z.boolean().optional(),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
47
|
const GetFileMetasSchema = z.object({
|
|
53
48
|
appName: z.string().min(1).max(255),
|
|
54
49
|
subId: z.uuid(),
|
|
@@ -73,11 +68,41 @@ const UploadFileMetaSchema = z.object({
|
|
|
73
68
|
## Examples
|
|
74
69
|
|
|
75
70
|
```ts
|
|
76
|
-
import { fileMetasQueryOptions, uploadFileMutationOptions } from "wcz-layout/data/client";
|
|
71
|
+
import { fileMetasQueryOptions, fileQueryOptions, uploadFileMutationOptions } from "wcz-layout/data/client";
|
|
72
|
+
import { Dropzone, FileViewer } from "wcz-layout/components";
|
|
73
|
+
import { useInView } from "wcz-layout/hooks";
|
|
74
|
+
|
|
75
|
+
// fetch metas + fetch first file
|
|
76
|
+
const { ref, inView } = useInView();
|
|
77
|
+
|
|
78
|
+
const { data: fileMetas = [] } = useQuery({
|
|
79
|
+
...fileMetasQueryOptions({ appName: packageJson.name, subId }),
|
|
80
|
+
enabled: inView,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { data: file } = useQuery({
|
|
84
|
+
...fileQueryOptions({ appName: packageJson.name, id: fileMetas[0]?.id ?? "" }),
|
|
85
|
+
enabled: !!fileMetas[0],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// upload
|
|
89
|
+
const [progress, setProgress] = useState(0);
|
|
90
|
+
const { mutateAsync: uploadFile } = useMutation(uploadFileMutationOptions({ queryClient }));
|
|
91
|
+
|
|
92
|
+
// Dropzone
|
|
93
|
+
const onDrop = async (acceptedFiles: File[]) => {
|
|
94
|
+
await Promise.allSettled(
|
|
95
|
+
acceptedFiles.map((file) =>
|
|
96
|
+
uploadFile({ appName: packageJson.name, subId, file, setProgress }),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
setUploadProgress(0);
|
|
100
|
+
};
|
|
77
101
|
|
|
78
|
-
|
|
102
|
+
<Dropzone onDrop={onDrop} progress={progress} />
|
|
79
103
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
// File Viewer - no manual fetch metas needed (default)
|
|
105
|
+
<FileViewer appName={packageJson.name} subId={subId}>
|
|
106
|
+
{(component) => <component.Grid />} // or component.List
|
|
107
|
+
</FileViewer>
|
|
83
108
|
```
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
2. Run `git branch --show-current`.
|
|
9
9
|
- **Not `master`** → run `git branch -M master`.
|
|
10
10
|
- **Already `master`** → continue.
|
|
11
|
-
3. Run `npx vp config` to install the Git hooks (pre-commit checks + automatic version bump).
|
|
11
|
+
3. Run `npx vp config` to install the Git hooks (pre-commit checks + automatic version bump). Running it again is a safe no-op.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Database Setup
|
|
2
2
|
|
|
3
|
+
> Create `.env.local` if it does not exist, then preserve its existing keys when setting database values. Values written here are temporary when the project uses Vault: the Vault Setup step moves them into the Vault `default` secret. Both `DATABASE_URL` and `DATABASE_SCHEMA` stay in `.env.local` — drizzle-kit (`db:studio`, `db:generate`) reads both.
|
|
4
|
+
|
|
3
5
|
## User Input
|
|
4
6
|
|
|
5
7
|
Ask: **"Do you already have a Postgres database created for this project? (yes/no)"**
|
|
@@ -32,6 +34,8 @@ docker run --name <project-name>-postgres -e POSTGRES_PASSWORD=<password> -d -p
|
|
|
32
34
|
- **Success** → continue.
|
|
33
35
|
- **Error** → stop and show the full error to the user.
|
|
34
36
|
|
|
35
|
-
5. Update `.env.local
|
|
37
|
+
5. Update `.env.local`:
|
|
38
|
+
- Set `DATABASE_URL=postgres://postgres:<password>@localhost:<PORT>/postgres`.
|
|
39
|
+
- Set `DATABASE_SCHEMA=public`.
|
|
36
40
|
6. Output exactly:
|
|
37
41
|
> ✅ Local Postgres database created successfully on port [PORT]
|
|
@@ -51,9 +51,9 @@ Wait for "continue", then proceed to Part C.
|
|
|
51
51
|
|
|
52
52
|
## Part C — Credentials
|
|
53
53
|
|
|
54
|
-
Ask: **"Please provide the CLIENT_ID
|
|
54
|
+
Ask: **"Please provide the CLIENT_ID for your Entra ID application."**
|
|
55
55
|
|
|
56
56
|
Once received:
|
|
57
57
|
|
|
58
|
-
1. Update `.env.local` → set `ENTRA_CLIENT_ID=<CLIENT_ID>`.
|
|
59
|
-
2. **CRITICAL**:
|
|
58
|
+
1. Update `.env.local` → set `ENTRA_CLIENT_ID=<CLIENT_ID>`. (If the project uses Vault, this value moves into the Vault `default` secret during the Vault Setup step.)
|
|
59
|
+
2. **CRITICAL**: Never ask for, print, or save the `CLIENT_SECRET`. The user enters it themselves in the Vault Setup step — into the Vault `default` secret, or directly into `.env.local` when not using Vault.
|
|
@@ -1,50 +1,71 @@
|
|
|
1
1
|
# Vault Setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Never print user-provided secret values into chat, files, or logs — reference only the env keys and where each value lives. The one value the AI generates itself is `SESSION_SECRET`: a fresh 64-char hex string via `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`. The template ships only `VITE_MUI_LICENSE_KEY` in `.env`; all project configuration goes into the gitignored `.env.local`, which may have been created by an earlier setup step and is normalized in this step.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## User Input
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Ask: **"Do you use Vault for this project? (yes/no)"**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Yes** → Branch A.
|
|
10
|
+
- **No** → Branch B.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
---
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
> 2. Find your project, open the "default" secret path, click 'Edit' (toggle 'View as JSON').
|
|
15
|
-
> 3. Fill in a JSON object with the following keys, then save. When finished, type 'done'.
|
|
14
|
+
## Branch A — Vault
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
### Part 1 — Vault secrets (user fills these in the Vault UI)
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
Output:
|
|
19
|
+
|
|
20
|
+
> 1. Navigate to your Vault UI and log in.
|
|
21
|
+
> 2. In your app's root path, create a secret named exactly `harborvault`: toggle 'JSON' mode, then copy-paste the JSON values from another app's `harborvault` secret and save.
|
|
22
|
+
> 3. Open your app's `default` secret, click 'Edit' (toggle 'View as JSON'), fill in these keys, then save. Provide user this in JSON to copy-paste — empty string values, except `SESSION_SECRET`, which you pre-fill with a freshly generated value:
|
|
23
|
+
>
|
|
24
|
+
> | Key | Value source |
|
|
25
|
+
> | ----------------------- | ----------------------------------------------------- |
|
|
26
|
+
> | `ENTRA_CLIENT_ID` | the Entra ID step |
|
|
27
|
+
> | `ENTRA_TENANT_ID` | same for all Wistron apps — copy from another project |
|
|
28
|
+
> | `ENTRA_CLIENT_SECRET` | your Entra ID application |
|
|
29
|
+
> | `SESSION_SECRET` | already pre-filled in the JSON — generated by the AI |
|
|
30
|
+
> | `DATABASE_SCHEMA` | the database step |
|
|
31
|
+
> | `DATABASE_AUTO_MIGRATE` | `true` unless you manage migrations manually |
|
|
32
|
+
> | `DATABASE_URL` | the database step |
|
|
33
|
+
>
|
|
34
|
+
> When finished, type 'done'.
|
|
25
35
|
|
|
26
36
|
Wait for "done".
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
### Part 2 — Normalize `.env.local`
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
Rewrite `.env.local` so it contains **only** the keys below. Never fill in secret values yourself — keep values empty except `DATABASE_URL` and `DATABASE_SCHEMA`, where you keep the values if the database step already set them (drizzle-kit reads both):
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
```dotenv
|
|
43
|
+
VAULT_ADDRESS=
|
|
44
|
+
VAULT_SECRET_PATH=
|
|
45
|
+
VAULT_USERNAME=
|
|
46
|
+
VAULT_PASSWORD=
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
> 2. Toggle 'JSON' mode.
|
|
38
|
-
> 3. Go to another existing project in your Vault, copy the JSON values from its `harborvault` secret, and paste them into this new one, then save.
|
|
39
|
-
>
|
|
40
|
-
> Once finished, type 'done'.
|
|
48
|
+
DATABASE_URL=
|
|
49
|
+
DATABASE_SCHEMA=
|
|
50
|
+
```
|
|
41
51
|
|
|
42
|
-
Wait for "done".
|
|
52
|
+
Then ask the user to fill in the empty values directly in `.env.local` (not in chat) and type 'done'. Wait for "done".
|
|
43
53
|
|
|
44
54
|
---
|
|
45
55
|
|
|
46
|
-
##
|
|
56
|
+
## Branch B — No Vault
|
|
57
|
+
|
|
58
|
+
Create `.env.local` to contain exactly these keys — keep any values already set by earlier steps, fill `SESSION_SECRET` with a freshly generated value, and leave the rest empty:
|
|
59
|
+
|
|
60
|
+
```dotenv
|
|
61
|
+
ENTRA_CLIENT_ID=
|
|
62
|
+
ENTRA_TENANT_ID=
|
|
63
|
+
ENTRA_CLIENT_SECRET=
|
|
64
|
+
SESSION_SECRET=<generated-64-char-hex>
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
DATABASE_SCHEMA=
|
|
67
|
+
DATABASE_AUTO_MIGRATE=
|
|
68
|
+
DATABASE_URL=
|
|
69
|
+
```
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
Then ask the user to fill in the empty values directly in `.env.local` (not in chat) and wait for "done".
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-C5-a_6W6.mjs","names":["createServerFn","serverEnv","getAppToken","getAccessToken","authenticationMiddleware","getFileUploadToken","method","middleware","optional","handler","token","endpoint","FILE_BASE_URL","mutationOptions","queryOptions","Upload","uuidv7","z","FileSchema","GetFileMetasSchema","UpdateFileMetaSchema","UploadFileMetaSchema","UploadFileResult","batchDeleteFiles","deleteFile","downloadFile","getFile","getFileMetas","getFileThumbnail","updateFileMeta","QueryClientParam","getFileUploadToken","QUERY_KEY","HOUR","baseOptions","staleTime","gcTime","refetchOnWindowFocus","streamToObjectUrl","stream","ReadableStream","Promise","blob","Response","URL","createObjectURL","saveBlob","Blob","fileName","url","anchor","document","createElement","href","download","click","revokeObjectURL","fileMetasQueryOptions","params","input","queryKey","appName","subId","queryFn","data","fileThumbnailQueryOptions","id","enabled","fileQueryOptions","downloadFileMutationOptions","mutationFn","fileExtension","openFileMutationOptions","Error","window","open","updateFileMetaMutationOptions","queryClient","onSettled","invalidateQueries","exact","deleteFileMutationOptions","batchDeleteFilesMutationOptions","uploadFileMutationOptions","file","setProgress","progress","token","endpoint","dotIndex","name","lastIndexOf","slice","resolve","reject","upload","chunkSize","metadata","mimeType","type","headers","Authorization","onProgress","bytesUploaded","bytesTotal","onError","onSuccess","start","result"],"sources":["../src/server/actions/file.ts","../src/data/client/file.ts"],"sourcesContent":["import { createServerFn } from \"@tanstack/react-start\";\r\nimport { serverEnv } from \"~/env\";\r\nimport { getAppToken } from \"~/lib/auth/msalServer\";\r\nimport { getAccessToken } from \"~/lib/auth/user\";\r\nimport { authenticationMiddleware } from \"~/middleware/authMiddleware\";\r\n\r\nexport const getFileUploadToken = createServerFn({ method: \"GET\" })\r\n .middleware([authenticationMiddleware({ optional: true })])\r\n .handler(async () => {\r\n let token: string;\r\n try {\r\n token = await getAccessToken(\"file\");\r\n } catch {\r\n token = await getAppToken(\"file\");\r\n }\r\n return { token, endpoint: `${serverEnv.FILE_BASE_URL}/v1/upload` };\r\n });\r\n","import { mutationOptions, queryOptions } from \"@tanstack/react-query\";\nimport { Upload } from \"tus-js-client\";\nimport { uuidv7 } from \"uuidv7\";\nimport type { z } from \"zod\";\nimport type {\n FileSchema,\n GetFileMetasSchema,\n UpdateFileMetaSchema,\n UploadFileMetaSchema,\n UploadFileResult,\n} from \"~/data/server/file\";\nimport {\n batchDeleteFiles,\n deleteFile,\n downloadFile,\n getFile,\n getFileMetas,\n getFileThumbnail,\n updateFileMeta,\n} from \"~/data/server/file\";\nimport type { QueryClientParam } from \"~/models/QueryClientParam\";\nimport { getFileUploadToken } from \"~/server/actions/file\";\n\nconst QUERY_KEY = \"file\";\nconst HOUR = 1000 * 60 * 60;\n\nconst baseOptions = {\n staleTime: HOUR,\n gcTime: HOUR,\n refetchOnWindowFocus: false,\n};\n\nasync function streamToObjectUrl(stream: ReadableStream): Promise<string> {\n const blob = await new Response(stream).blob();\n return URL.createObjectURL(blob);\n}\n\nfunction saveBlob(blob: Blob, fileName: string): void {\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = url;\n anchor.download = fileName;\n anchor.click();\n URL.revokeObjectURL(url);\n}\n\nexport const fileMetasQueryOptions = (params: z.input<typeof GetFileMetasSchema>) =>\n queryOptions({\n queryKey: [QUERY_KEY, \"meta\", params.appName, params.subId],\n queryFn: () => getFileMetas({ data: params }),\n ...baseOptions,\n });\n\nexport const fileThumbnailQueryOptions = (params: z.input<typeof FileSchema>) =>\n queryOptions({\n queryKey: [QUERY_KEY, \"thumbnail\", params.appName, params.id],\n queryFn: async () => streamToObjectUrl(await getFileThumbnail({ data: params })),\n enabled: !!params.id,\n ...baseOptions,\n });\n\nexport const fileQueryOptions = (params: z.input<typeof FileSchema>) =>\n queryOptions({\n queryKey: [QUERY_KEY, params.appName, params.id],\n queryFn: async () => streamToObjectUrl(await getFile({ data: params })),\n enabled: !!params.id,\n ...baseOptions,\n });\n\nexport const downloadFileMutationOptions = () =>\n mutationOptions({\n mutationFn: async (\n data: z.input<typeof FileSchema> & { fileName: string; fileExtension: string },\n ) => {\n const stream = await downloadFile({ data });\n saveBlob(await new Response(stream).blob(), `${data.fileName}.${data.fileExtension}`);\n },\n });\n\nexport const openFileMutationOptions = () =>\n mutationOptions<void, Error, z.input<typeof FileSchema>>({\n mutationFn: async (data) => {\n const stream = await getFile({ data });\n window.open(await streamToObjectUrl(stream));\n },\n });\n\nexport const updateFileMetaMutationOptions = ({ queryClient }: QueryClientParam) =>\n mutationOptions({\n mutationFn: (data: z.input<typeof UpdateFileMetaSchema>) => updateFileMeta({ data }),\n onSettled: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEY, \"meta\"], exact: false }),\n });\n\nexport const deleteFileMutationOptions = ({ queryClient }: QueryClientParam) =>\n mutationOptions({\n mutationFn: (data: z.input<typeof FileSchema>) => deleteFile({ data }),\n onSettled: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEY, \"meta\"], exact: false }),\n });\n\nexport const batchDeleteFilesMutationOptions = ({ queryClient }: QueryClientParam) =>\n mutationOptions({\n mutationFn: (data: z.input<typeof GetFileMetasSchema>) => batchDeleteFiles({ data }),\n onSettled: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEY, \"meta\"], exact: false }),\n });\n\nexport const uploadFileMutationOptions = ({ queryClient }: QueryClientParam) =>\n mutationOptions({\n mutationFn: async ({\n appName,\n subId,\n file,\n setProgress,\n }: z.input<typeof UploadFileMetaSchema> & {\n setProgress?: (progress: number) => void;\n }): Promise<UploadFileResult> => {\n const { token, endpoint } = await getFileUploadToken();\n const id = uuidv7();\n const dotIndex = file.name.lastIndexOf(\".\");\n const fileExtension = dotIndex >= 0 ? file.name.slice(dotIndex + 1) : \"\";\n\n await new Promise<void>((resolve, reject) => {\n const upload = new Upload(file, {\n endpoint,\n chunkSize: 1_048_576,\n metadata: { id, appName, subId, fileName: file.name, fileExtension, mimeType: file.type },\n headers: { Authorization: `Bearer ${token}` },\n onProgress: (bytesUploaded, bytesTotal) =>\n setProgress?.((bytesUploaded / bytesTotal) * 100),\n onError: reject,\n onSuccess: () => resolve(),\n });\n upload.start();\n });\n\n return { id, appName, subId, fileName: file.name };\n },\n onSuccess: (result) =>\n queryClient.invalidateQueries({\n queryKey: [QUERY_KEY, \"meta\", result.appName, result.subId],\n exact: false,\n }),\n });\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-DM1x40eJ.mjs","names":["createServerFn","setResponseHeaders","https","Readable","DefaultHttpStack","Upload","uuidv7","z","serverEnv","getAppToken","getAccessToken","apiMiddleware","authenticationMiddleware","FileMeta","FileMetaSchema","GetFileMetasSchema","object","appName","string","min","max","subId","uuid","FileSchema","id","UpdateFileMetaSchema","extend","fileName","DeleteFileSchema","BatchDeleteFilesSchema","UploadFileResult","UploadFileMetaSchema","file","instanceof","File","getFileMetas","method","validator","middleware","baseURL","scopeKey","handler","data","context","queryParams","URLSearchParams","append","fileMetas","api","request","Array","url","toString","getFileThumbnail","response","responseType","Headers","headers","toWeb","ReadableStream","getFile","downloadFile","updateFileMeta","deleteFile","batchDeleteFiles","insecureHttpsAgent","Agent","rejectUnauthorized","uploadFile","FormData","parse","get","Promise","accessToken","stream","fromWeb","dotIndex","name","lastIndexOf","fileExtension","slice","resolve","reject","upload","endpoint","FILE_BASE_URL","httpStack","agent","uploadSize","size","chunkSize","metadata","mimeType","type","Authorization","onError","onSuccess","start"],"sources":["../src/data/server/file.ts"],"sourcesContent":["import { createServerFn } from \"@tanstack/react-start\";\nimport { setResponseHeaders } from \"@tanstack/react-start/server\";\nimport https from \"node:https\";\nimport { Readable } from \"node:stream\";\nimport { DefaultHttpStack, Upload } from \"tus-js-client\";\nimport { uuidv7 } from \"uuidv7\";\nimport { z } from \"zod\";\nimport { serverEnv } from \"~/env\";\nimport { getAppToken } from \"~/lib/auth/msalServer\";\nimport { getAccessToken } from \"~/lib/auth/user\";\nimport { apiMiddleware } from \"~/middleware/apiMiddleware\";\nimport { authenticationMiddleware } from \"~/middleware/authMiddleware\";\nimport type { FileMeta } from \"~/models/file/FileMeta\";\nimport { FileMetaSchema } from \"~/models/file/FileMeta\";\n\nexport const GetFileMetasSchema = z.object({\n appName: z.string().min(1).max(255),\n subId: z.uuid(),\n});\n\nexport const FileSchema = z.object({\n appName: z.string().min(1).max(255),\n id: z.uuid(),\n});\n\nexport const UpdateFileMetaSchema = FileMetaSchema.extend({\n fileName: z.string().min(1).max(255),\n});\n\nconst DeleteFileSchema = FileSchema;\n\nconst BatchDeleteFilesSchema = GetFileMetasSchema;\n\nexport interface UploadFileResult {\n id: string;\n appName: string;\n subId: string;\n fileName: string;\n}\n\nexport const UploadFileMetaSchema = z.object({\n appName: z.string().min(1).max(255),\n subId: z.uuid(),\n file: z.instanceof(File),\n});\n\nexport const getFileMetas = createServerFn({ method: \"GET\" })\n .validator(GetFileMetasSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"subId\", data.subId);\n\n const { data: fileMetas } = await context.api.request<Array<FileMeta>>({\n url: `/v1/meta?${queryParams.toString()}`,\n method: \"GET\",\n });\n return fileMetas;\n });\n\nexport const getFileThumbnail = createServerFn({ method: \"GET\" })\n .validator(FileSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"id\", data.id);\n\n const response = await context.api.request<Readable>({\n url: `/v1/thumbnail?${queryParams.toString()}`,\n method: \"GET\",\n responseType: \"stream\",\n });\n\n setResponseHeaders(\n new Headers({\n \"Content-Type\": response.headers[\"content-type\"] as string,\n \"Cache-Control\": \"private, max-age=3600, immutable\",\n }),\n );\n return Readable.toWeb(response.data) as ReadableStream;\n });\n\nexport const getFile = createServerFn({ method: \"GET\" })\n .validator(FileSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"id\", data.id);\n\n const response = await context.api.request<Readable>({\n url: `/v1?${queryParams.toString()}`,\n method: \"GET\",\n responseType: \"stream\",\n });\n\n setResponseHeaders(\n new Headers({\n \"Content-Type\": response.headers[\"content-type\"] as string,\n \"Cache-Control\": \"private, max-age=3600, immutable\",\n }),\n );\n return Readable.toWeb(response.data) as ReadableStream;\n });\n\nexport const downloadFile = createServerFn({ method: \"GET\" })\n .validator(FileSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"id\", data.id);\n\n const response = await context.api.request<Readable>({\n url: `/v1/download?${queryParams.toString()}`,\n method: \"GET\",\n responseType: \"stream\",\n });\n\n setResponseHeaders(\n new Headers({\n \"Content-Type\": response.headers[\"content-type\"] as string,\n \"Cache-Control\": \"private, max-age=3600, immutable\",\n }),\n );\n return Readable.toWeb(response.data) as ReadableStream;\n });\n\nexport const updateFileMeta = createServerFn({ method: \"POST\" })\n .validator(UpdateFileMetaSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"id\", data.id);\n\n await context.api.request({ url: `/v1/meta?${queryParams.toString()}`, method: \"PUT\", data });\n });\n\nexport const deleteFile = createServerFn({ method: \"POST\" })\n .validator(DeleteFileSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"id\", data.id);\n\n await context.api.request({ url: `/v1?${queryParams.toString()}`, method: \"DELETE\" });\n });\n\nexport const batchDeleteFiles = createServerFn({ method: \"POST\" })\n .validator(BatchDeleteFilesSchema)\n .middleware([apiMiddleware({ baseURL: \"FILE_BASE_URL\", scopeKey: \"file\" })])\n .handler(async ({ data, context }) => {\n const queryParams = new URLSearchParams();\n queryParams.append(\"appName\", data.appName);\n queryParams.append(\"subId\", data.subId);\n\n await context.api.request({ url: `/v1?${queryParams.toString()}`, method: \"DELETE\" });\n });\n\nconst insecureHttpsAgent = new https.Agent({ rejectUnauthorized: false });\n\nexport const uploadFile = createServerFn({ method: \"POST\" })\n // File inputs must travel as multipart FormData — a File nested in a plain\n // payload object can't be seroval-serialized, so the request never fires.\n .validator((data: FormData) =>\n UploadFileMetaSchema.parse({\n appName: data.get(\"appName\"),\n subId: data.get(\"subId\"),\n file: data.get(\"file\"),\n }),\n )\n .middleware([authenticationMiddleware()])\n .handler(async ({ data }): Promise<UploadFileResult> => {\n // Prefer the signed-in user's delegated token so the file service stamps\n // createdBy; fall back to the app identity when there's no user session.\n let accessToken: string;\n try {\n accessToken = await getAccessToken(\"file\");\n } catch {\n accessToken = await getAppToken(\"file\");\n }\n // Stream instead of buffering the whole file: in-process callers (e.g. a File\n // from fs.openAsBlob) upload arbitrarily large files without holding them in RAM.\n const stream = Readable.fromWeb(data.file.stream() as import(\"node:stream/web\").ReadableStream);\n const id = uuidv7();\n const dotIndex = data.file.name.lastIndexOf(\".\");\n const fileExtension = dotIndex >= 0 ? data.file.name.slice(dotIndex + 1) : \"\";\n\n // Server-originated edge cases only — browser uploads go direct to the file\n // service via uploadFileMutationOptions (client) with the user's OBO token.\n await new Promise<void>((resolve, reject) => {\n const upload = new Upload(stream, {\n endpoint: `${serverEnv.FILE_BASE_URL}/v1/upload`,\n httpStack: new DefaultHttpStack({ agent: insecureHttpsAgent }),\n uploadSize: data.file.size,\n chunkSize: 1_048_576,\n metadata: {\n id,\n appName: data.appName,\n subId: data.subId,\n fileName: data.file.name,\n fileExtension,\n mimeType: data.file.type,\n },\n headers: { Authorization: `Bearer ${accessToken}` },\n onError: reject,\n onSuccess: () => resolve(),\n });\n upload.start();\n });\n\n return {\n id,\n appName: data.appName,\n subId: data.subId,\n fileName: data.file.name,\n };\n });\n"],"mappings":"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"}
|