@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.
Files changed (161) hide show
  1. package/README.md +136 -57
  2. package/bin/create-fidj.cjs +62 -0
  3. package/generators/app/index.js +100 -189
  4. package/generators/app/templates/typescript/README.md +82 -0
  5. package/generators/app/templates/typescript/app.config.json +14 -0
  6. package/generators/app/templates/typescript/gitignore +5 -0
  7. package/generators/app/templates/typescript/package.json +25 -0
  8. package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
  9. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
  10. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-400-latin.woff2 +0 -0
  11. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
  12. package/generators/app/templates/typescript/public/fonts/IBMPlexMono-500-latin.woff2 +0 -0
  13. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin-ext.woff2 +0 -0
  14. package/generators/app/templates/typescript/public/fonts/IBMPlexSans-400_600-latin.woff2 +0 -0
  15. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin-ext.woff2 +0 -0
  16. package/generators/app/templates/typescript/public/fonts/InstrumentSerif-400-latin.woff2 +0 -0
  17. package/generators/app/templates/typescript/public/index.html +16 -0
  18. package/generators/app/templates/typescript/scripts/build.mjs +49 -0
  19. package/generators/app/templates/typescript/scripts/privacy-check.mjs +21 -0
  20. package/generators/app/templates/typescript/scripts/render-content.mjs +14 -0
  21. package/generators/app/templates/typescript/server/data-store.ts +123 -0
  22. package/generators/app/templates/typescript/server/index.ts +338 -0
  23. package/generators/app/templates/typescript/server/privacy-adapter.ts +21 -0
  24. package/generators/app/templates/typescript/src/content.ts +491 -0
  25. package/generators/app/templates/typescript/src/fonts.css +92 -0
  26. package/generators/app/templates/typescript/src/main.ts +251 -0
  27. package/generators/app/templates/typescript/src/service-agreement.ts +40 -0
  28. package/generators/app/templates/typescript/src/style.css +916 -0
  29. package/generators/app/templates/typescript/src/tokens.css +56 -0
  30. package/generators/app/templates/typescript/test/server.test.mjs +287 -0
  31. package/generators/app/templates/typescript/tsconfig.json +15 -0
  32. package/lib/app-module.cjs +64 -0
  33. package/lib/scaffold.cjs +214 -0
  34. package/package.json +15 -13
  35. package/generators/app/templates/app2018/.travis.yml +0 -35
  36. package/generators/app/templates/app2018/LICENSE +0 -21
  37. package/generators/app/templates/app2018/Procfile +0 -1
  38. package/generators/app/templates/app2018/README.md +0 -49
  39. package/generators/app/templates/app2018/config.xml +0 -91
  40. package/generators/app/templates/app2018/gulpfile.js +0 -63
  41. package/generators/app/templates/app2018/ionic.config.json +0 -9
  42. package/generators/app/templates/app2018/package.json +0 -90
  43. package/generators/app/templates/app2018/platforms/README.md +0 -1
  44. package/generators/app/templates/app2018/resources/README.md +0 -6
  45. package/generators/app/templates/app2018/resources/icon.png +0 -0
  46. package/generators/app/templates/app2018/resources/marketing-01-ipad.png +0 -0
  47. package/generators/app/templates/app2018/resources/marketing-01-iphone.png +0 -0
  48. package/generators/app/templates/app2018/resources/marketing-02-ipad.png +0 -0
  49. package/generators/app/templates/app2018/resources/marketing-02-iphone.png +0 -0
  50. package/generators/app/templates/app2018/resources/marketing-03-ipad.png +0 -0
  51. package/generators/app/templates/app2018/resources/marketing-03-iphone.png +0 -0
  52. package/generators/app/templates/app2018/resources/marketing.json +0 -19
  53. package/generators/app/templates/app2018/resources/splash.png +0 -0
  54. package/generators/app/templates/app2018/src/app/app.component.ts +0 -88
  55. package/generators/app/templates/app2018/src/app/app.html +0 -19
  56. package/generators/app/templates/app2018/src/app/app.module.ts +0 -40
  57. package/generators/app/templates/app2018/src/app/app.scss +0 -16
  58. package/generators/app/templates/app2018/src/app/main.ts +0 -5
  59. package/generators/app/templates/app2018/src/app/shared/card.model.ts +0 -10
  60. package/generators/app/templates/app2018/src/app/shared/profile.service.ts +0 -46
  61. package/generators/app/templates/app2018/src/app/shared/shared.module.ts +0 -35
  62. package/generators/app/templates/app2018/src/app/shared/storage.service.ts +0 -11
  63. package/generators/app/templates/app2018/src/app/shared/version.const.ts +0 -5
  64. package/generators/app/templates/app2018/src/assets/icon/favicon.ico +0 -0
  65. package/generators/app/templates/app2018/src/assets/imgs/gg.png +0 -0
  66. package/generators/app/templates/app2018/src/assets/imgs/github.png +0 -0
  67. package/generators/app/templates/app2018/src/assets/imgs/login.svg +0 -5668
  68. package/generators/app/templates/app2018/src/assets/imgs/logo.png +0 -0
  69. package/generators/app/templates/app2018/src/index.html +0 -49
  70. package/generators/app/templates/app2018/src/manifest.json +0 -13
  71. package/generators/app/templates/app2018/src/pages/home/home.html +0 -56
  72. package/generators/app/templates/app2018/src/pages/home/home.scss +0 -3
  73. package/generators/app/templates/app2018/src/pages/home/home.ts +0 -126
  74. package/generators/app/templates/app2018/src/pages/login/login.html +0 -56
  75. package/generators/app/templates/app2018/src/pages/login/login.scss +0 -44
  76. package/generators/app/templates/app2018/src/pages/login/login.ts +0 -59
  77. package/generators/app/templates/app2018/src/pages/profile/profile.html +0 -33
  78. package/generators/app/templates/app2018/src/pages/profile/profile.scss +0 -3
  79. package/generators/app/templates/app2018/src/pages/profile/profile.ts +0 -36
  80. package/generators/app/templates/app2018/src/service-worker.js +0 -31
  81. package/generators/app/templates/app2018/src/theme/variables.scss +0 -88
  82. package/generators/app/templates/app2018/tests/karma.unit-tests.conf.js +0 -84
  83. package/generators/app/templates/app2018/tests/unit/app.test.js +0 -10
  84. package/generators/app/templates/app2018/tsconfig.json +0 -28
  85. package/generators/app/templates/app2018/tslint.json +0 -8
  86. package/generators/app/templates/app2018/web.js +0 -21
  87. package/generators/app/templates/app2021/.travis.yml +0 -32
  88. package/generators/app/templates/app2021/LICENSE +0 -21
  89. package/generators/app/templates/app2021/Procfile +0 -1
  90. package/generators/app/templates/app2021/README.md +0 -45
  91. package/generators/app/templates/app2021/angular.json +0 -188
  92. package/generators/app/templates/app2021/browserslist +0 -12
  93. package/generators/app/templates/app2021/ionic.config.json +0 -9
  94. package/generators/app/templates/app2021/karma.conf.js +0 -31
  95. package/generators/app/templates/app2021/package-lock.json +0 -5
  96. package/generators/app/templates/app2021/package.json +0 -77
  97. package/generators/app/templates/app2021/platforms/README.md +0 -1
  98. package/generators/app/templates/app2021/resources/README.md +0 -6
  99. package/generators/app/templates/app2021/resources/icon.png +0 -0
  100. package/generators/app/templates/app2021/resources/marketing-01-ipad.png +0 -0
  101. package/generators/app/templates/app2021/resources/marketing-01-iphone.png +0 -0
  102. package/generators/app/templates/app2021/resources/marketing-02-ipad.png +0 -0
  103. package/generators/app/templates/app2021/resources/marketing-02-iphone.png +0 -0
  104. package/generators/app/templates/app2021/resources/marketing-03-ipad.png +0 -0
  105. package/generators/app/templates/app2021/resources/marketing-03-iphone.png +0 -0
  106. package/generators/app/templates/app2021/resources/marketing.json +0 -19
  107. package/generators/app/templates/app2021/resources/splash.png +0 -0
  108. package/generators/app/templates/app2021/src/app/app-routing.module.ts +0 -25
  109. package/generators/app/templates/app2021/src/app/app.component.html +0 -3
  110. package/generators/app/templates/app2021/src/app/app.component.scss +0 -0
  111. package/generators/app/templates/app2021/src/app/app.component.spec.ts +0 -50
  112. package/generators/app/templates/app2021/src/app/app.component.ts +0 -28
  113. package/generators/app/templates/app2021/src/app/app.module.ts +0 -24
  114. package/generators/app/templates/app2021/src/app/content/content-routing.module.ts +0 -16
  115. package/generators/app/templates/app2021/src/app/content/content.module.ts +0 -20
  116. package/generators/app/templates/app2021/src/app/content/content.page.html +0 -31
  117. package/generators/app/templates/app2021/src/app/content/content.page.scss +0 -0
  118. package/generators/app/templates/app2021/src/app/content/content.page.ts +0 -26
  119. package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.html +0 -4
  120. package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.scss +0 -4
  121. package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.spec.ts +0 -24
  122. package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.ts +0 -16
  123. package/generators/app/templates/app2021/src/app/explore-container/explore-container.module.ts +0 -14
  124. package/generators/app/templates/app2021/src/app/my/my-routing.module.ts +0 -36
  125. package/generators/app/templates/app2021/src/app/my/my.module.ts +0 -19
  126. package/generators/app/templates/app2021/src/app/my/my.page.html +0 -15
  127. package/generators/app/templates/app2021/src/app/my/my.page.scss +0 -1
  128. package/generators/app/templates/app2021/src/app/my/my.page.spec.ts +0 -26
  129. package/generators/app/templates/app2021/src/app/my/my.page.ts +0 -25
  130. package/generators/app/templates/app2021/src/app/profile/profile-routing.module.ts +0 -16
  131. package/generators/app/templates/app2021/src/app/profile/profile.module.ts +0 -20
  132. package/generators/app/templates/app2021/src/app/profile/profile.page.html +0 -49
  133. package/generators/app/templates/app2021/src/app/profile/profile.page.scss +0 -0
  134. package/generators/app/templates/app2021/src/app/profile/profile.page.spec.ts +0 -25
  135. package/generators/app/templates/app2021/src/app/profile/profile.page.ts +0 -44
  136. package/generators/app/templates/app2021/src/app/shared/const.ts +0 -3
  137. package/generators/app/templates/app2021/src/app/signin/signin-routing.module.ts +0 -16
  138. package/generators/app/templates/app2021/src/app/signin/signin.module.ts +0 -18
  139. package/generators/app/templates/app2021/src/app/signin/signin.page.html +0 -53
  140. package/generators/app/templates/app2021/src/app/signin/signin.page.scss +0 -36
  141. package/generators/app/templates/app2021/src/app/signin/signin.page.ts +0 -69
  142. package/generators/app/templates/app2021/src/assets/icon/favicon.png +0 -0
  143. package/generators/app/templates/app2021/src/assets/shapes.svg +0 -1
  144. package/generators/app/templates/app2021/src/environments/environment.prod.ts +0 -3
  145. package/generators/app/templates/app2021/src/environments/environment.ts +0 -16
  146. package/generators/app/templates/app2021/src/global.scss +0 -26
  147. package/generators/app/templates/app2021/src/index.html +0 -26
  148. package/generators/app/templates/app2021/src/main.ts +0 -12
  149. package/generators/app/templates/app2021/src/polyfills.ts +0 -67
  150. package/generators/app/templates/app2021/src/test.ts +0 -20
  151. package/generators/app/templates/app2021/src/theme/variables.scss +0 -232
  152. package/generators/app/templates/app2021/src/zone-flags.ts +0 -5
  153. package/generators/app/templates/app2021/tests/protractor.conf.js +0 -36
  154. package/generators/app/templates/app2021/tests/src/app.e2e-spec.ts +0 -14
  155. package/generators/app/templates/app2021/tests/src/app.po.ts +0 -11
  156. package/generators/app/templates/app2021/tests/tsconfig.json +0 -13
  157. package/generators/app/templates/app2021/tsconfig.app.json +0 -18
  158. package/generators/app/templates/app2021/tsconfig.json +0 -23
  159. package/generators/app/templates/app2021/tsconfig.spec.json +0 -18
  160. package/generators/app/templates/app2021/tslint.json +0 -149
  161. package/generators/app/templates/app2021/web.js +0 -21
@@ -0,0 +1,82 @@
1
+ # Your Fidj TypeScript app
2
+
3
+ A generated TypeScript client with Fidj sign-in and app-scoped privacy. The generator selects the mode from its inputs:
4
+
5
+ - With `--content`: public HTML plus account/privacy controls, built to static `www/` assets and optional `CNAME`. No app backend is required; the client calls Fidj directly.
6
+ - Without `--content`: Studio Notes, with a Node backend enforcing live Owner/Editor roles for private notes.
7
+
8
+ The public title, welcome, HTML content and endpoints are in `app.config.json`. Edit the original generator command and regenerate for repeatable changes. Content HTML is trusted developer source.
9
+
10
+ ## Run
11
+
12
+ Use Node 22 or later. Copy `.env.example` to `.env`, set the public app ID and Fidj API URL, then:
13
+
14
+ ```sh
15
+ npm install
16
+ npm run build
17
+ npm start
18
+ ```
19
+
20
+ Open http://localhost:8200. For content apps, `npm run build-prod` produces static `www/` assets; the Node process is only an optional preview. For Studio Notes, the backend must run for protected operations. Configure Fidj’s allowed origins for your app domain. No app signing key belongs in the browser or this starter’s public configuration.
21
+
22
+ The current unreleased generator requires the matching `@ofidj/node` 3.6.24 SDK. Until publication, generate with `--sdk-path /absolute/path/to/fidj-node/dist` after building that SDK. Local file dependencies belong only in generated development output; use registry dependencies for release.
23
+
24
+ ## Structure
25
+
26
+ - `src/content.ts`: static content app account, live roles and privacy controls.
27
+ - `src/main.ts`: `FidjNodeService.init`, login, browser session and views.
28
+ - `server/index.ts`: `verifyAppSession` on every protected request; live roles decide whether a note can be saved. Changing a role takes effect on the next backend request, even if a browser still displays old roles.
29
+ - `test/server.test.mjs`: fresh HTTP servers verify role changes, revoked/foreign sessions, outage failure, per-user notes and privacy scope.
30
+ - `scripts/build.mjs`: browser and Node builds using [esbuild](https://esbuild.github.io/getting-started/), with a separate TypeScript typecheck.
31
+
32
+ Use TDD: add and run a failing behavior test before implementation (red), make it pass (green), then refactor and run `npm test`. Change maintained template source and regenerate to verify that every fresh app receives the fix.
33
+
34
+ ## Privacy scope
35
+
36
+ Optional preferences and their history belong to this app. In the static content app, export covers the Fidj-held membership records and no independent app database exists. In Studio Notes, export includes the Fidj membership and the starter’s notes. Leaving through this app removes its Fidj membership and locally held notes. App owners must resolve ownership first.
37
+
38
+ Notes persist in `FIDJ_DATA_DIR`. With the adapter registered below, direct export/departure from Fidj includes these notes. Failed erasure keeps a request in Fidj for automatic retry with backoff; people can also retry explicitly. The API pauses after eight failed automatic attempts and exposes a needs-attention state. Account-wide identity deletion, real terms/purposes and a full production retention policy remain separate integration work. The demo agreement is explicitly a placeholder, not a legal policy.
39
+
40
+ The browser SDK stores the app’s session in this origin’s local storage. Use HTTPS and maintain a strict content security policy when hosting. The backend delegates signature and session-revocation validation to the configured Fidj API and fails closed when it cannot verify a session.
41
+
42
+ ## Local scenario
43
+
44
+ For the Notes fixture only, `LOCAL_DEMO=true` with the loopback API enables synthetic Alex/Maya/Sam sign-in shortcuts. Content/module entries keep test accounts in developer documentation. These accounts must be seeded by your local Fidj API; the starter does not create accounts or seed data remotely. Alex is the owner, and the owner console can grant Maya Editor access.
45
+
46
+ ## Entry flow
47
+
48
+ The generated content app opens on `/#/signin`. Sign in, or choose **Enter anonymously** when enabled by `--anonymous true`, to open `/#/content`, containing the supplied HTML. Signed-in users can open **My privacy** separately. Sign-out and departure return to the sign-in screen. Anonymous content is public; this navigation flow is not a security boundary for static assets.
49
+
50
+ Set `--anonymous false` in the generator command to remove anonymous entry and route unsigned visitors back to sign-in. This controls navigation; static HTML/assets remain public. Put confidential content behind a server-authorized endpoint.
51
+
52
+ ## Application modules
53
+
54
+ When generated with `--module`, successful sign-in opens the copied module configured by `moduleEntry`. It runs under `/module/` on the same origin and must validate sessions/permissions independently. Its entry contains a `fidj-signin` meta URL for returning to this shared sign-in page. Module code is public static output; user data belongs behind authorized APIs. Rebuild the maintained module source and regenerate to update it; never patch its copied files here.
55
+
56
+ ## Connect app data rights
57
+
58
+ Set `FIDJ_PRIVACY_ADAPTER_KEY` to a private random secret of at least 32 characters. Keep it exclusively in server environments; it is separate from an app JWT signing key. Register the app on the Fidj API server:
59
+
60
+ ```json
61
+ {"YOUR_APP_ID":{"url":"https://your-app.example/fidj/privacy","key":"SAME_PRIVATE_RANDOM_SECRET"}}
62
+ ```
63
+
64
+ Set that JSON as `FIDJ_PRIVACY_ADAPTERS` on the API. Endpoint registration is operator-managed in this milestone. HTTPS is required; explicit local mode also permits loopback HTTP. The local workspace launcher configures Studio Notes automatically. Requests contain `appId`, `subject`, `operation` and `requestId`; headers contain a millisecond timestamp and SHA-256 HMAC of `timestamp + "." + JSON.stringify(body)`. Requests older than five minutes are rejected; redirects are not followed. The handler must return the matching `requestId` and `status: "completed"` only after durable success.
65
+
66
+ Keep `FIDJ_DATA_DIR` outside public assets and generated output. Use one writer process per directory. Replace `server/data-store.ts` with a transactional database implementation for multiple instances. Do not weaken the authorization check inside note writes or the atomic erase/receipt transaction. Duplicate erasures preserve later membership data. Minimal receipts contain a hashed subject and completion timestamp; receipts older than 30 days are pruned on the next write. Backups and unregistered external data remain outside this handler’s scope.
67
+
68
+
69
+ ## Check and rehearse
70
+
71
+ After starting the Notes backend with its server-only adapter secret, run `npm run privacy:check`. The signed check confirms export/erase capabilities and writable storage without accessing user records. Override `FIDJ_APP_URL` for a deployed HTTPS backend. Keep this secret out of browser configuration.
72
+
73
+ Run `npm run privacy:rehearse` for an isolated temporary-store HTTP scenario covering live roles, scoped export, persistence, failure and idempotent erasure. It never points at your running data directory. Groups are managed in the Fidj owner console; direct and group roles are checked live by the backend.
74
+
75
+ ## Agreement at sign-in
76
+
77
+ Sign-in and account creation require the unchecked service-agreement checkbox.
78
+ Read the app's current text/version from the dialog before accepting. If the
79
+ agreement cannot load, the form stays blocked. The API records the accepted
80
+ version per app; optional privacy choices are separate. The app owner must replace
81
+ the default demo agreement using the Fidj app's `configurationAsJSON.serviceAgreement`
82
+ (`version` and `text`) before release. No generated-source edit is needed.
@@ -0,0 +1,14 @@
1
+ {
2
+ "appId": "fidj-example",
3
+ "apiEndpoint": "https://api.sandbox.fidj.ovh/v3",
4
+ "dashboardUrl": "https://fidj.ovh",
5
+ "title": "My app",
6
+ "localDemo": false,
7
+ "allowAnonymous": true,
8
+ "moduleEntry": "",
9
+ "description": "A space to explore, with an account that puts you in control.",
10
+ "highlights": [],
11
+ "badges": [],
12
+ "logo": "./fidj-logo.png",
13
+ "favicon": "./fidj-logo.png"
14
+ }
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ .fidj-data/
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fidj-typescript-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc --noEmit && node scripts/build.mjs",
11
+ "start": "node --env-file=.env dist/server.cjs",
12
+ "test": "npm run build && node --test test/*.test.mjs",
13
+ "build-prod": "npm run build",
14
+ "privacy:check": "node --env-file=.env scripts/privacy-check.mjs",
15
+ "privacy:rehearse": "npm run build && node --test test/server.test.mjs"
16
+ },
17
+ "dependencies": {
18
+ "@ofidj/node": "^3.6.24"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "esbuild": "0.28.2",
23
+ "typescript": "5.9.3"
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>My workspace · Fidj</title>
7
+ <link rel="icon" href="/fidj-logo.png" />
8
+ <link rel="stylesheet" href="/main.css" />
9
+ <script type="module" src="/main.js"></script>
10
+ </head>
11
+ <body>
12
+ <div id="app">
13
+ <main><h1>Opening your workspace…</h1></main>
14
+ </div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,49 @@
1
+ import { build } from "esbuild";
2
+ import { cp, mkdir, rm, readFile, writeFile, readdir } from "node:fs/promises";
3
+ const config = JSON.parse(await readFile("app.config.json", "utf8"));
4
+ const contentApp = typeof config.content === "string";
5
+ await rm("dist", { recursive: true, force: true });
6
+ await mkdir("dist/public", { recursive: true });
7
+ await cp("public", "dist/public", { recursive: true });
8
+ await Promise.all([
9
+ build({
10
+ entryPoints: [contentApp ? "src/content.ts" : "src/main.ts"],
11
+ bundle: true,
12
+ platform: "browser",
13
+ target: "es2022",
14
+ outfile: "dist/public/main.js",
15
+ sourcemap: true,
16
+ // The design system's fonts are copied with public/; esbuild must leave
17
+ // their url()s alone rather than try to resolve them at build time.
18
+ external: ["*.woff2"],
19
+ }),
20
+ build({
21
+ entryPoints: ["server/index.ts"],
22
+ bundle: true,
23
+ packages: "external",
24
+ platform: "node",
25
+ target: "node22",
26
+ format: "cjs",
27
+ outfile: "dist/server.cjs",
28
+ }),
29
+ ]);
30
+ if (contentApp) {
31
+ const { renderContent } = await import("./render-content.mjs");
32
+ await writeFile("dist/public/index.html", renderContent(config));
33
+ await rm("www", { recursive: true, force: true });
34
+ await cp("dist/public", "www", { recursive: true });
35
+ if (config.domain) await writeFile("www/CNAME", config.domain + "\n");
36
+ }
37
+ await cp("dist/public/index.html", "dist/public/app.html");
38
+
39
+ const moduleFiles = {};
40
+ async function collect(directory, relative = "") {
41
+ for (const item of await readdir(directory, { withFileTypes: true })) {
42
+ const name = relative + item.name;
43
+ if (item.isDirectory())
44
+ await collect(directory + "/" + item.name, name + "/");
45
+ else moduleFiles["/" + name] = name;
46
+ }
47
+ }
48
+ await collect("dist/public");
49
+ await writeFile("dist/public-files.json", JSON.stringify(moduleFiles));
@@ -0,0 +1,21 @@
1
+ import {createHmac, randomUUID} from 'node:crypto';
2
+
3
+ try {
4
+ const key = process.env.FIDJ_PRIVACY_ADAPTER_KEY;
5
+ const appId = process.env.FIDJ_APP_ID;
6
+ if (!appId || !key || key.length < 32) throw new Error('Set FIDJ_APP_ID and a private FIDJ_PRIVACY_ADAPTER_KEY of at least 32 characters in .env.');
7
+ const url = new URL(process.env.FIDJ_APP_URL || `http://127.0.0.1:${process.env.PORT || 8200}`);
8
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname)))) throw new Error('Use a trusted HTTPS app URL, or loopback HTTP for development.');
9
+ url.pathname = url.pathname.replace(/\/$/, '') + '/fidj/privacy';
10
+ const timestamp = String(Date.now());
11
+ const data = {appId, subject: 'readiness-check', operation: 'check', requestId: randomUUID()};
12
+ const text = JSON.stringify(data);
13
+ const response = await fetch(url, {method: 'POST', headers: {'Content-Type': 'application/json', 'x-fidj-timestamp': timestamp, 'x-fidj-signature': createHmac('sha256', key).update(timestamp + '.' + text).digest('hex')}, body: text, signal: AbortSignal.timeout(5000), redirect: 'error'});
14
+ if (!response.ok) throw new Error('The app-data handler could not be verified. Check the URL, shared secret and server.');
15
+ const result = await response.json();
16
+ if (result.requestId !== data.requestId || result.status !== 'completed' || result.storage !== 'ready' || !['export', 'erase'].every(capability => result.capabilities?.includes(capability))) throw new Error('The handler did not confirm both operations and writable storage.');
17
+ console.log('Ready: app-data export, erasure and writable storage verified. No user data was created or erased.');
18
+ } catch (error) {
19
+ console.error(error.message || 'The readiness check failed.');
20
+ process.exitCode = 1;
21
+ }
@@ -0,0 +1,14 @@
1
+ const escape = (value) =>
2
+ String(value ?? "").replace(
3
+ /[&<>"']/g,
4
+ (char) =>
5
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
6
+ char
7
+ ],
8
+ );
9
+ export function renderContent(config) {
10
+ // HTML is supplied by the developer at generation time, never by an app visitor.
11
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="referrer" content="no-referrer"><title>${escape(config.title)}</title><link rel="icon" href="${escape(config.favicon)}"><link rel="stylesheet" href="./main.css"></head><body>
12
+ <header class="topbar"><a class="brand" href="#/content"><img src="${escape(config.logo)}" alt="">${escape(config.title)}</a></header>
13
+ <main><template id="public-content"><section class="card public-content"><h1>${escape(config.welcome)}</h1><div>${config.content}</div></section></template><div id="app" aria-live="polite"></div><footer>Built with Fidj · Your choices belong to this app.</footer></main><script type="module" src="./main.js"></script></body></html>`;
14
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ open,
5
+ rename,
6
+ readdir,
7
+ unlink,
8
+ } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+
12
+ export interface Note {
13
+ id: string;
14
+ title: string;
15
+ body: string;
16
+ createdAt: string;
17
+ }
18
+ interface State {
19
+ notes: Record<string, Note[]>;
20
+ receipts: Record<string, { subjectHash: string; completedAt: string }>;
21
+ }
22
+
23
+ // One writer process per directory. Replace this adapter with your database for multi-instance hosting.
24
+ export class DataStore {
25
+ private queue: Promise<unknown> = Promise.resolve();
26
+ constructor(
27
+ private directory: string,
28
+ private appId = "",
29
+ ) {}
30
+ private async state(): Promise<State> {
31
+ try {
32
+ return JSON.parse(
33
+ await readFile(join(this.directory, "data.json"), "utf8"),
34
+ );
35
+ } catch (error) {
36
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
37
+ return { notes: {}, receipts: {} };
38
+ throw error;
39
+ }
40
+ }
41
+ private transact<T>(task: (state: State) => T | Promise<T>): Promise<T> {
42
+ const operation = this.queue.then(async () => {
43
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
44
+ // Interrupted writes must not leave a second copy of erased data behind.
45
+ for (const name of await readdir(this.directory)) {
46
+ if (/^[a-f0-9-]{36}\.tmp$/.test(name))
47
+ await unlink(join(this.directory, name));
48
+ }
49
+ const state = await this.state();
50
+ for (const [id, receipt] of Object.entries(state.receipts)) {
51
+ if (Date.parse(receipt.completedAt) < Date.now() - 30 * 86400000)
52
+ delete state.receipts[id];
53
+ }
54
+ const result = await task(state);
55
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
56
+ const temporary = join(this.directory, randomUUID() + ".tmp");
57
+ try {
58
+ const file = await open(temporary, "wx", 0o600);
59
+ try {
60
+ await file.writeFile(JSON.stringify(state));
61
+ await file.sync();
62
+ } finally {
63
+ await file.close();
64
+ }
65
+ await rename(temporary, join(this.directory, "data.json"));
66
+ } finally {
67
+ await unlink(temporary).catch((error) => {
68
+ if (error.code !== "ENOENT") throw error;
69
+ });
70
+ }
71
+ const directory = await open(this.directory, "r");
72
+ try {
73
+ await directory.sync();
74
+ } finally {
75
+ await directory.close();
76
+ }
77
+ return result;
78
+ });
79
+ this.queue = operation.catch(() => {});
80
+ return operation;
81
+ }
82
+ async check() {
83
+ await this.transact(() => undefined);
84
+ return {storage: "ready", capabilities: ["export", "erase"]};
85
+ }
86
+ async notes(subject: string): Promise<Note[]> {
87
+ await this.queue;
88
+ return (await this.state()).notes[this.key(subject)] || [];
89
+ }
90
+ add(subject: string, note: Note, authorize: () => Promise<unknown>) {
91
+ return this.transact(async (state) => {
92
+ await authorize();
93
+ const key = this.key(subject);
94
+ const notes = state.notes[key] || [];
95
+ if (notes.length >= 100) throw new Error("Note limit reached.");
96
+ state.notes[key] = [...notes, note];
97
+ });
98
+ }
99
+ erase(subject: string, requestId: string) {
100
+ return this.transact((state) => {
101
+ const subjectHash = this.key(subject);
102
+ const existing = state.receipts[requestId];
103
+ if (existing && existing.subjectHash !== subjectHash)
104
+ throw new Error("Receipt identity mismatch.");
105
+ // Re-delivery of a completed request must not erase data from a later membership.
106
+ if (existing)
107
+ return {
108
+ requestId,
109
+ status: "completed",
110
+ completedAt: existing.completedAt,
111
+ };
112
+ delete state.notes[subjectHash];
113
+ const completedAt = new Date().toISOString();
114
+ state.receipts[requestId] = { subjectHash, completedAt };
115
+ return { requestId, status: "completed", completedAt };
116
+ });
117
+ }
118
+ private key(subject: string) {
119
+ return createHash("sha256")
120
+ .update(this.appId + ":" + subject)
121
+ .digest("hex");
122
+ }
123
+ }