@ofidj/generator-fidj 1.0.0 → 1.0.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 +136 -57
- package/bin/create-fidj.cjs +62 -0
- package/generators/app/index.js +100 -189
- package/generators/app/templates/typescript/README.md +82 -0
- package/generators/app/templates/typescript/app.config.json +14 -0
- package/generators/app/templates/typescript/gitignore +5 -0
- package/generators/app/templates/typescript/package.json +25 -0
- package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
- package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
- package/generators/app/templates/typescript/public/index.html +16 -0
- package/generators/app/templates/typescript/scripts/build.mjs +49 -0
- package/generators/app/templates/typescript/scripts/privacy-check.mjs +21 -0
- package/generators/app/templates/typescript/scripts/render-content.mjs +14 -0
- package/generators/app/templates/typescript/server/data-store.ts +123 -0
- package/generators/app/templates/typescript/server/index.ts +338 -0
- package/generators/app/templates/typescript/server/privacy-adapter.ts +21 -0
- package/generators/app/templates/typescript/src/content.ts +491 -0
- package/generators/app/templates/typescript/src/fonts.css +92 -0
- package/generators/app/templates/typescript/src/main.ts +251 -0
- package/generators/app/templates/typescript/src/service-agreement.ts +40 -0
- package/generators/app/templates/typescript/src/style.css +916 -0
- package/generators/app/templates/typescript/src/tokens.css +56 -0
- package/generators/app/templates/typescript/test/server.test.mjs +287 -0
- package/generators/app/templates/typescript/tsconfig.json +15 -0
- package/lib/app-module.cjs +64 -0
- package/lib/scaffold.cjs +214 -0
- package/package.json +15 -13
- package/generators/app/templates/app2018/.travis.yml +0 -35
- package/generators/app/templates/app2018/LICENSE +0 -21
- package/generators/app/templates/app2018/Procfile +0 -1
- package/generators/app/templates/app2018/README.md +0 -49
- package/generators/app/templates/app2018/config.xml +0 -91
- package/generators/app/templates/app2018/gulpfile.js +0 -63
- package/generators/app/templates/app2018/ionic.config.json +0 -9
- package/generators/app/templates/app2018/package.json +0 -90
- package/generators/app/templates/app2018/platforms/README.md +0 -1
- package/generators/app/templates/app2018/resources/README.md +0 -6
- package/generators/app/templates/app2018/resources/icon.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing.json +0 -19
- package/generators/app/templates/app2018/resources/splash.png +0 -0
- package/generators/app/templates/app2018/src/app/app.component.ts +0 -88
- package/generators/app/templates/app2018/src/app/app.html +0 -19
- package/generators/app/templates/app2018/src/app/app.module.ts +0 -40
- package/generators/app/templates/app2018/src/app/app.scss +0 -16
- package/generators/app/templates/app2018/src/app/main.ts +0 -5
- package/generators/app/templates/app2018/src/app/shared/card.model.ts +0 -10
- package/generators/app/templates/app2018/src/app/shared/profile.service.ts +0 -46
- package/generators/app/templates/app2018/src/app/shared/shared.module.ts +0 -35
- package/generators/app/templates/app2018/src/app/shared/storage.service.ts +0 -11
- package/generators/app/templates/app2018/src/app/shared/version.const.ts +0 -5
- package/generators/app/templates/app2018/src/assets/icon/favicon.ico +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/gg.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/github.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/login.svg +0 -5668
- package/generators/app/templates/app2018/src/assets/imgs/logo.png +0 -0
- package/generators/app/templates/app2018/src/index.html +0 -49
- package/generators/app/templates/app2018/src/manifest.json +0 -13
- package/generators/app/templates/app2018/src/pages/home/home.html +0 -56
- package/generators/app/templates/app2018/src/pages/home/home.scss +0 -3
- package/generators/app/templates/app2018/src/pages/home/home.ts +0 -126
- package/generators/app/templates/app2018/src/pages/login/login.html +0 -56
- package/generators/app/templates/app2018/src/pages/login/login.scss +0 -44
- package/generators/app/templates/app2018/src/pages/login/login.ts +0 -59
- package/generators/app/templates/app2018/src/pages/profile/profile.html +0 -33
- package/generators/app/templates/app2018/src/pages/profile/profile.scss +0 -3
- package/generators/app/templates/app2018/src/pages/profile/profile.ts +0 -36
- package/generators/app/templates/app2018/src/service-worker.js +0 -31
- package/generators/app/templates/app2018/src/theme/variables.scss +0 -88
- package/generators/app/templates/app2018/tests/karma.unit-tests.conf.js +0 -84
- package/generators/app/templates/app2018/tests/unit/app.test.js +0 -10
- package/generators/app/templates/app2018/tsconfig.json +0 -28
- package/generators/app/templates/app2018/tslint.json +0 -8
- package/generators/app/templates/app2018/web.js +0 -21
- package/generators/app/templates/app2021/.travis.yml +0 -32
- package/generators/app/templates/app2021/LICENSE +0 -21
- package/generators/app/templates/app2021/Procfile +0 -1
- package/generators/app/templates/app2021/README.md +0 -45
- package/generators/app/templates/app2021/angular.json +0 -188
- package/generators/app/templates/app2021/browserslist +0 -12
- package/generators/app/templates/app2021/ionic.config.json +0 -9
- package/generators/app/templates/app2021/karma.conf.js +0 -31
- package/generators/app/templates/app2021/package-lock.json +0 -5
- package/generators/app/templates/app2021/package.json +0 -77
- package/generators/app/templates/app2021/platforms/README.md +0 -1
- package/generators/app/templates/app2021/resources/README.md +0 -6
- package/generators/app/templates/app2021/resources/icon.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing.json +0 -19
- package/generators/app/templates/app2021/resources/splash.png +0 -0
- package/generators/app/templates/app2021/src/app/app-routing.module.ts +0 -25
- package/generators/app/templates/app2021/src/app/app.component.html +0 -3
- package/generators/app/templates/app2021/src/app/app.component.scss +0 -0
- package/generators/app/templates/app2021/src/app/app.component.spec.ts +0 -50
- package/generators/app/templates/app2021/src/app/app.component.ts +0 -28
- package/generators/app/templates/app2021/src/app/app.module.ts +0 -24
- package/generators/app/templates/app2021/src/app/content/content-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/content/content.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/content/content.page.html +0 -31
- package/generators/app/templates/app2021/src/app/content/content.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/content/content.page.ts +0 -26
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.html +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.scss +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.spec.ts +0 -24
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.ts +0 -16
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.module.ts +0 -14
- package/generators/app/templates/app2021/src/app/my/my-routing.module.ts +0 -36
- package/generators/app/templates/app2021/src/app/my/my.module.ts +0 -19
- package/generators/app/templates/app2021/src/app/my/my.page.html +0 -15
- package/generators/app/templates/app2021/src/app/my/my.page.scss +0 -1
- package/generators/app/templates/app2021/src/app/my/my.page.spec.ts +0 -26
- package/generators/app/templates/app2021/src/app/my/my.page.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/profile/profile.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/profile/profile.page.html +0 -49
- package/generators/app/templates/app2021/src/app/profile/profile.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/profile/profile.page.spec.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile.page.ts +0 -44
- package/generators/app/templates/app2021/src/app/shared/const.ts +0 -3
- package/generators/app/templates/app2021/src/app/signin/signin-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/signin/signin.module.ts +0 -18
- package/generators/app/templates/app2021/src/app/signin/signin.page.html +0 -53
- package/generators/app/templates/app2021/src/app/signin/signin.page.scss +0 -36
- package/generators/app/templates/app2021/src/app/signin/signin.page.ts +0 -69
- package/generators/app/templates/app2021/src/assets/icon/favicon.png +0 -0
- package/generators/app/templates/app2021/src/assets/shapes.svg +0 -1
- package/generators/app/templates/app2021/src/environments/environment.prod.ts +0 -3
- package/generators/app/templates/app2021/src/environments/environment.ts +0 -16
- package/generators/app/templates/app2021/src/global.scss +0 -26
- package/generators/app/templates/app2021/src/index.html +0 -26
- package/generators/app/templates/app2021/src/main.ts +0 -12
- package/generators/app/templates/app2021/src/polyfills.ts +0 -67
- package/generators/app/templates/app2021/src/test.ts +0 -20
- package/generators/app/templates/app2021/src/theme/variables.scss +0 -232
- package/generators/app/templates/app2021/src/zone-flags.ts +0 -5
- package/generators/app/templates/app2021/tests/protractor.conf.js +0 -36
- package/generators/app/templates/app2021/tests/src/app.e2e-spec.ts +0 -14
- package/generators/app/templates/app2021/tests/src/app.po.ts +0 -11
- package/generators/app/templates/app2021/tests/tsconfig.json +0 -13
- package/generators/app/templates/app2021/tsconfig.app.json +0 -18
- package/generators/app/templates/app2021/tsconfig.json +0 -23
- package/generators/app/templates/app2021/tsconfig.spec.json +0 -18
- package/generators/app/templates/app2021/tslint.json +0 -149
- package/generators/app/templates/app2021/web.js +0 -21
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join, extname } from "node:path";
|
|
4
|
+
import { DataStore } from "./data-store";
|
|
5
|
+
import { verifyPrivacyRequest } from "./privacy-adapter";
|
|
6
|
+
export { DataStore } from "./data-store";
|
|
7
|
+
import { verifyAppSession, SessionVerificationError } from "@ofidj/node";
|
|
8
|
+
|
|
9
|
+
export interface Settings {
|
|
10
|
+
appId: string;
|
|
11
|
+
apiEndpoint: string;
|
|
12
|
+
dashboardUrl: string;
|
|
13
|
+
title: string;
|
|
14
|
+
localDemo: boolean;
|
|
15
|
+
}
|
|
16
|
+
class HttpError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
public status: number,
|
|
19
|
+
message: string,
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function body(req: IncomingMessage): Promise<any> {
|
|
26
|
+
let value = "";
|
|
27
|
+
for await (const chunk of req) {
|
|
28
|
+
value += chunk;
|
|
29
|
+
if (Buffer.byteLength(value) > 16384)
|
|
30
|
+
throw new HttpError(413, "Request too large.");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(value || "{}");
|
|
34
|
+
} catch {
|
|
35
|
+
throw new HttpError(400, "Invalid JSON.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function json(res: ServerResponse, status: number, value: unknown) {
|
|
39
|
+
res.writeHead(status, {
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"Cache-Control": "no-store",
|
|
42
|
+
"X-Content-Type-Options": "nosniff",
|
|
43
|
+
});
|
|
44
|
+
res.end(JSON.stringify(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createApp(
|
|
48
|
+
settings: Settings,
|
|
49
|
+
options: { dataDir?: string; adapterKey?: string; store?: DataStore } = {},
|
|
50
|
+
) {
|
|
51
|
+
const store =
|
|
52
|
+
options.store ||
|
|
53
|
+
new DataStore(
|
|
54
|
+
options.dataDir || join(process.cwd(), ".fidj-data"),
|
|
55
|
+
settings.appId,
|
|
56
|
+
);
|
|
57
|
+
const upstream = async (
|
|
58
|
+
token: string,
|
|
59
|
+
suffix: string,
|
|
60
|
+
method = "GET",
|
|
61
|
+
data?: unknown,
|
|
62
|
+
) => {
|
|
63
|
+
let response: Response;
|
|
64
|
+
try {
|
|
65
|
+
response = await fetch(
|
|
66
|
+
`${settings.apiEndpoint}/me/apps/${encodeURIComponent(settings.appId)}${suffix}`,
|
|
67
|
+
{
|
|
68
|
+
method,
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
74
|
+
signal: AbortSignal.timeout(5000),
|
|
75
|
+
redirect: "error",
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
} catch {
|
|
79
|
+
throw new HttpError(503, "Fidj is unavailable. Please retry.");
|
|
80
|
+
}
|
|
81
|
+
const result = await response.json().catch(() => ({}));
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
throw new HttpError(
|
|
84
|
+
response.status,
|
|
85
|
+
result.status || "Fidj could not complete this action.",
|
|
86
|
+
);
|
|
87
|
+
return { status: response.status, data: result };
|
|
88
|
+
};
|
|
89
|
+
return createServer(async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const pathname = new URL(req.url || "/", "http://localhost").pathname;
|
|
92
|
+
if (pathname === "/fidj/privacy" && req.method === "POST") {
|
|
93
|
+
const input = await body(req);
|
|
94
|
+
if (
|
|
95
|
+
!verifyPrivacyRequest(
|
|
96
|
+
options.adapterKey,
|
|
97
|
+
String(req.headers["x-fidj-timestamp"] || ""),
|
|
98
|
+
String(req.headers["x-fidj-signature"] || ""),
|
|
99
|
+
input,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
throw new HttpError(401, "Invalid privacy request.");
|
|
103
|
+
if (
|
|
104
|
+
input.appId !== settings.appId ||
|
|
105
|
+
typeof input.subject !== "string" ||
|
|
106
|
+
!input.subject ||
|
|
107
|
+
input.subject.length > 200 ||
|
|
108
|
+
typeof input.requestId !== "string" ||
|
|
109
|
+
!/^[a-zA-Z0-9-]{1,100}$/.test(input.requestId)
|
|
110
|
+
)
|
|
111
|
+
throw new HttpError(400, "Invalid privacy scope.");
|
|
112
|
+
if (input.operation === "check") return json(res, 200, {requestId: input.requestId, status: "completed", ...await store.check()});
|
|
113
|
+
if (input.operation === "export")
|
|
114
|
+
return json(res, 200, {
|
|
115
|
+
requestId: input.requestId,
|
|
116
|
+
status: "completed",
|
|
117
|
+
data: { notes: await store.notes(input.subject) },
|
|
118
|
+
coverage: "Notes stored by this app",
|
|
119
|
+
});
|
|
120
|
+
if (input.operation === "erase")
|
|
121
|
+
return json(
|
|
122
|
+
res,
|
|
123
|
+
200,
|
|
124
|
+
await store.erase(input.subject, input.requestId),
|
|
125
|
+
);
|
|
126
|
+
throw new HttpError(400, "Unsupported privacy operation.");
|
|
127
|
+
}
|
|
128
|
+
if (pathname === "/api/config" && req.method === "GET")
|
|
129
|
+
return json(res, 200, settings);
|
|
130
|
+
if (pathname === "/api/health" && req.method === "GET")
|
|
131
|
+
return json(res, 200, { status: "ready" });
|
|
132
|
+
if (pathname.startsWith("/api/")) {
|
|
133
|
+
const authorization = req.headers.authorization || "";
|
|
134
|
+
if (!authorization.startsWith("Bearer "))
|
|
135
|
+
throw new HttpError(401, "Sign in to continue.");
|
|
136
|
+
const token = authorization.slice(7);
|
|
137
|
+
const session = await verifyAppSession(token, settings);
|
|
138
|
+
if (pathname === "/api/session" && req.method === "GET")
|
|
139
|
+
return json(res, 200, session);
|
|
140
|
+
if (pathname === "/api/notes" && req.method === "GET")
|
|
141
|
+
return json(res, 200, { notes: await store.notes(session.subject) });
|
|
142
|
+
if (pathname === "/api/notes" && req.method === "POST") {
|
|
143
|
+
if (!session.roles.some((role) => ["Owner", "Editor"].includes(role)))
|
|
144
|
+
throw new HttpError(
|
|
145
|
+
403,
|
|
146
|
+
"An Editor or Owner role is required to save a note.",
|
|
147
|
+
);
|
|
148
|
+
const input = await body(req);
|
|
149
|
+
if (
|
|
150
|
+
typeof input.title !== "string" ||
|
|
151
|
+
!input.title.trim() ||
|
|
152
|
+
input.title.length > 120 ||
|
|
153
|
+
typeof input.body !== "string" ||
|
|
154
|
+
input.body.length > 5000
|
|
155
|
+
)
|
|
156
|
+
throw new HttpError(
|
|
157
|
+
400,
|
|
158
|
+
"Add a title (up to 120 characters) and a note (up to 5,000 characters).",
|
|
159
|
+
);
|
|
160
|
+
const current = await store.notes(session.subject);
|
|
161
|
+
if (current.length >= 100)
|
|
162
|
+
throw new HttpError(
|
|
163
|
+
409,
|
|
164
|
+
"This starter supports 100 notes per person.",
|
|
165
|
+
);
|
|
166
|
+
const note = {
|
|
167
|
+
id: crypto.randomUUID(),
|
|
168
|
+
title: input.title.trim(),
|
|
169
|
+
body: input.body,
|
|
170
|
+
createdAt: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
await store.add(session.subject, note, () =>
|
|
173
|
+
verifyAppSession(token, settings),
|
|
174
|
+
);
|
|
175
|
+
return json(res, 201, { note });
|
|
176
|
+
}
|
|
177
|
+
if (pathname === "/api/privacy" && req.method === "GET") {
|
|
178
|
+
const [consent, history] = await Promise.all([
|
|
179
|
+
upstream(token, "/consents"),
|
|
180
|
+
upstream(token, "/consents/history"),
|
|
181
|
+
]);
|
|
182
|
+
return json(res, 200, {
|
|
183
|
+
consent: consent.data,
|
|
184
|
+
history: history.data.history,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (pathname === "/api/privacy" && req.method === "PUT") {
|
|
188
|
+
const input = await body(req);
|
|
189
|
+
const data: Record<string, unknown> = { source: "profile" };
|
|
190
|
+
for (const purpose of [
|
|
191
|
+
"terms",
|
|
192
|
+
"analytics",
|
|
193
|
+
"communications",
|
|
194
|
+
"optionalData",
|
|
195
|
+
])
|
|
196
|
+
if (input[purpose] !== undefined) data[purpose] = input[purpose];
|
|
197
|
+
if (input.terms === true) data.termsVersion = "starter-demo-1";
|
|
198
|
+
const result = await upstream(token, "/consents", "PUT", data);
|
|
199
|
+
return json(res, result.status, result.data);
|
|
200
|
+
}
|
|
201
|
+
if (pathname === "/api/privacy/export" && req.method === "GET") {
|
|
202
|
+
const identity = await upstream(token, "/export");
|
|
203
|
+
return json(res, 200, {
|
|
204
|
+
exportedAt: new Date().toISOString(),
|
|
205
|
+
fidj: identity.data,
|
|
206
|
+
app: identity.data.applicationData || {
|
|
207
|
+
notes: await store.notes(session.subject),
|
|
208
|
+
},
|
|
209
|
+
coverage:
|
|
210
|
+
"Fidj records for this membership plus this app’s persisted notes. No other apps or independent external systems.",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (pathname === "/api/privacy/leave" && req.method === "DELETE") {
|
|
214
|
+
const input = await body(req);
|
|
215
|
+
if (input.confirm !== settings.appId)
|
|
216
|
+
throw new HttpError(400, "Confirm this app before leaving.");
|
|
217
|
+
const connection = await upstream(token, "/consents");
|
|
218
|
+
if (!connection.data.appDataConnected)
|
|
219
|
+
throw new HttpError(
|
|
220
|
+
409,
|
|
221
|
+
"The app owner must connect the data-erasure handler before departure can include your notes.",
|
|
222
|
+
);
|
|
223
|
+
const result = await upstream(token, "", "DELETE", {
|
|
224
|
+
confirm: settings.appId,
|
|
225
|
+
});
|
|
226
|
+
return json(res, result.status, {
|
|
227
|
+
...result.data,
|
|
228
|
+
appNotes: result.data.appData || "not_connected",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
throw new HttpError(404, "Unknown operation.");
|
|
232
|
+
}
|
|
233
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
234
|
+
throw new HttpError(405, "Method not allowed.");
|
|
235
|
+
const files: Record<string, [string, string]> = {
|
|
236
|
+
"/": ["index.html", "text/html"],
|
|
237
|
+
"/index.html": ["index.html", "text/html"],
|
|
238
|
+
"/app": ["app.html", "text/html"],
|
|
239
|
+
"/hero.gif": ["hero.gif", "image/gif"],
|
|
240
|
+
"/main.js": ["main.js", "text/javascript"],
|
|
241
|
+
"/main.css": ["main.css", "text/css"],
|
|
242
|
+
"/fidj-logo.png": ["fidj-logo.png", "image/png"],
|
|
243
|
+
};
|
|
244
|
+
// Beyond the named files above, three directories are served from the
|
|
245
|
+
// build manifest: the assembled application module, the design system's
|
|
246
|
+
// self-hosted fonts, and any logo or favicon the app supplied. The
|
|
247
|
+
// manifest only resolves paths the build actually produced.
|
|
248
|
+
if (
|
|
249
|
+
!files[pathname] &&
|
|
250
|
+
(pathname.startsWith("/module/") ||
|
|
251
|
+
pathname.startsWith("/fonts/") ||
|
|
252
|
+
pathname.startsWith("/brand/"))
|
|
253
|
+
) {
|
|
254
|
+
const manifest = JSON.parse(
|
|
255
|
+
await readFile(join(__dirname, "public-files.json"), "utf8"),
|
|
256
|
+
);
|
|
257
|
+
const asset =
|
|
258
|
+
manifest[pathname] ||
|
|
259
|
+
(pathname.endsWith("/")
|
|
260
|
+
? manifest[pathname + "index.html"]
|
|
261
|
+
: undefined);
|
|
262
|
+
const types: Record<string, string> = {
|
|
263
|
+
".html": "text/html",
|
|
264
|
+
".js": "text/javascript",
|
|
265
|
+
".css": "text/css",
|
|
266
|
+
".json": "application/json",
|
|
267
|
+
".png": "image/png",
|
|
268
|
+
".svg": "image/svg+xml",
|
|
269
|
+
".gif": "image/gif",
|
|
270
|
+
".ico": "image/x-icon",
|
|
271
|
+
".woff": "font/woff",
|
|
272
|
+
".woff2": "font/woff2",
|
|
273
|
+
};
|
|
274
|
+
if (typeof asset === "string")
|
|
275
|
+
files[pathname] = [
|
|
276
|
+
asset,
|
|
277
|
+
types[extname(asset)] || "application/octet-stream",
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
const file = files[pathname];
|
|
281
|
+
if (!file) throw new HttpError(404, "Not found.");
|
|
282
|
+
const content = await readFile(join(__dirname, "public", file[0]));
|
|
283
|
+
const apiOrigin = new URL(settings.apiEndpoint).origin;
|
|
284
|
+
res.writeHead(200, {
|
|
285
|
+
"Content-Type": file[1],
|
|
286
|
+
"X-Content-Type-Options": "nosniff",
|
|
287
|
+
"Referrer-Policy": "no-referrer",
|
|
288
|
+
"Content-Security-Policy": `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https:; connect-src 'self' ${apiOrigin}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'`,
|
|
289
|
+
});
|
|
290
|
+
res.end(req.method === "HEAD" ? undefined : content);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const status =
|
|
293
|
+
error instanceof HttpError || error instanceof SessionVerificationError
|
|
294
|
+
? error.status
|
|
295
|
+
: 500;
|
|
296
|
+
json(res, status, {
|
|
297
|
+
message:
|
|
298
|
+
status === 500
|
|
299
|
+
? "The app could not complete this action."
|
|
300
|
+
: (error as Error).message,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (require.main === module) {
|
|
307
|
+
const appId = process.env.FIDJ_APP_ID;
|
|
308
|
+
if (!appId || !process.env.FIDJ_API_ENDPOINT)
|
|
309
|
+
throw new Error("Set FIDJ_APP_ID and FIDJ_API_ENDPOINT in .env.");
|
|
310
|
+
const apiEndpoint = process.env.FIDJ_API_ENDPOINT.replace(/\/$/, "");
|
|
311
|
+
const api = new URL(apiEndpoint);
|
|
312
|
+
if (
|
|
313
|
+
!["https:", "http:"].includes(api.protocol) ||
|
|
314
|
+
api.username ||
|
|
315
|
+
api.password ||
|
|
316
|
+
api.search ||
|
|
317
|
+
api.hash
|
|
318
|
+
)
|
|
319
|
+
throw new Error("Invalid Fidj API URL.");
|
|
320
|
+
const port = Number(process.env.PORT || 8200);
|
|
321
|
+
const host = process.env.HOST || "127.0.0.1";
|
|
322
|
+
const settings = {
|
|
323
|
+
appId,
|
|
324
|
+
apiEndpoint,
|
|
325
|
+
dashboardUrl: process.env.FIDJ_DASHBOARD_URL || "https://fidj.ovh",
|
|
326
|
+
title: process.env.APP_TITLE || "My workspace",
|
|
327
|
+
localDemo:
|
|
328
|
+
process.env.LOCAL_DEMO === "true" &&
|
|
329
|
+
host === "127.0.0.1" &&
|
|
330
|
+
["localhost", "127.0.0.1"].includes(api.hostname),
|
|
331
|
+
};
|
|
332
|
+
createApp(settings, {
|
|
333
|
+
dataDir: process.env.FIDJ_DATA_DIR,
|
|
334
|
+
adapterKey: process.env.FIDJ_PRIVACY_ADAPTER_KEY,
|
|
335
|
+
}).listen(port, host, () =>
|
|
336
|
+
console.log(`${settings.title}: http://${host}:${port}`),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function verifyPrivacyRequest(
|
|
4
|
+
key: string | undefined,
|
|
5
|
+
timestamp: string,
|
|
6
|
+
signature: string,
|
|
7
|
+
payload: unknown,
|
|
8
|
+
) {
|
|
9
|
+
if (
|
|
10
|
+
!key ||
|
|
11
|
+
key.length < 32 ||
|
|
12
|
+
!/^\d+$/.test(timestamp) ||
|
|
13
|
+
Math.abs(Date.now() - Number(timestamp)) > 300000 ||
|
|
14
|
+
!/^[a-f0-9]{64}$/.test(signature)
|
|
15
|
+
)
|
|
16
|
+
return false;
|
|
17
|
+
const expected = createHmac("sha256", key)
|
|
18
|
+
.update(timestamp + "." + JSON.stringify(payload))
|
|
19
|
+
.digest();
|
|
20
|
+
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
|
|
21
|
+
}
|