@carlonicora/nextjs-jsonapi 4.0.7 → 4.1.0
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/{BlockNoteEditor-J7EF7BRX.js → BlockNoteEditor-6CBMNJPU.js} +9 -9
- package/dist/{BlockNoteEditor-J7EF7BRX.js.map → BlockNoteEditor-6CBMNJPU.js.map} +1 -1
- package/dist/{BlockNoteEditor-KOHN3SSR.mjs → BlockNoteEditor-7N66JABP.mjs} +2 -2
- package/dist/billing/index.js +335 -335
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-3W3VVX2N.mjs → chunk-HQKTQBUC.mjs} +89 -29
- package/dist/chunk-HQKTQBUC.mjs.map +1 -0
- package/dist/{chunk-3M7D6NXU.js → chunk-MJVEKDM4.js} +320 -260
- package/dist/chunk-MJVEKDM4.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +10 -1
- package/dist/components/index.d.ts +10 -1
- package/dist/components/index.js +4 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +3 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/analytics/index.js +88 -88
- package/dist/features/analytics/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +75 -75
- package/dist/features/tokenusage/index.mjs +1 -1
- package/dist/next-config/index.d.mts +8 -1
- package/dist/next-config/index.d.ts +8 -1
- package/dist/next-config/index.js +31 -9
- package/dist/next-config/index.js.map +1 -1
- package/dist/next-config/index.mjs +31 -9
- package/dist/next-config/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/features/company/components/details/CompanyContent.tsx +9 -2
- package/src/features/company/components/forms/CompanyEditor.tsx +2 -0
- package/src/features/company/hooks/useSubscriptionStatus.test.ts +76 -10
- package/src/features/company/hooks/useSubscriptionStatus.ts +9 -9
- package/src/features/how-to/components/containers/HowToCommand.spec.tsx +79 -6
- package/src/features/how-to/components/containers/HowToCommand.tsx +12 -2
- package/src/features/how-to/components/forms/HowToSelector.spec.tsx +85 -0
- package/src/features/how-to/components/forms/HowToSelector.tsx +1 -0
- package/src/features/role/components/forms/UserRoleAdd.spec.tsx +71 -0
- package/src/features/role/components/forms/UserRoleAdd.tsx +1 -0
- package/src/features/user/components/forms/UserSelector.spec.tsx +85 -0
- package/src/features/user/components/forms/UserSelector.tsx +1 -0
- package/src/features/user/components/lists/UserListInAdd.spec.tsx +84 -0
- package/src/features/user/components/lists/UserListInAdd.tsx +1 -0
- package/src/features/user/components/widgets/UserSearchPopover.spec.tsx +68 -0
- package/src/features/user/components/widgets/UserSearchPopover.tsx +1 -0
- package/src/next-config/__tests__/createNextConfig.test.ts +45 -3
- package/src/next-config/csp.ts +25 -4
- package/src/next-config/index.ts +19 -12
- package/src/shadcnui/custom/multi-select.spec.tsx +70 -0
- package/src/shadcnui/custom/multi-select.tsx +26 -2
- package/src/shadcnui/custom/multiple-selector.spec.tsx +111 -0
- package/src/shadcnui/custom/multiple-selector.tsx +33 -10
- package/src/shadcnui/ui/command.spec.ts +26 -0
- package/src/shadcnui/ui/command.tsx +13 -1
- package/dist/chunk-3M7D6NXU.js.map +0 -1
- package/dist/chunk-3W3VVX2N.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-KOHN3SSR.mjs.map → BlockNoteEditor-7N66JABP.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/next-config/index.js","../../src/next-config/csp.ts","../../src/next-config/headers.ts","../../src/next-config/index.ts"],"names":[],"mappings":"AAAA;AACE;AACF,uDAA6B;AAC7B;AACA;ACoBA,IAAM,gBAAA,EAA+E;AAAA,EACnF,MAAA,EAAQ;AAAA,IACN,YAAA,EAAc,CAAC,uBAAuB,CAAA;AAAA,IACtC,SAAA,EAAW,CAAC,sBAAsB,CAAA;AAAA,IAClC,aAAA,EAAe,CAAC,wBAAwB,CAAA;AAAA,IACxC,WAAA,EAAa,CAAC,uBAAA,EAAyB,0BAA0B;AAAA,EACnE,CAAA;AAAA,EACA,UAAA,EAAY;AAAA,IACV,YAAA,EAAc,CAAC,6BAA6B,CAAA;AAAA,IAC5C,SAAA,EAAW,CAAC,6BAAA,EAA+B,0BAA0B,CAAA;AAAA,IACrE,aAAA,EAAe,CAAC,6BAAA,EAA+B,+BAA+B;AAAA,EAChF,CAAA;AAAA,EACA,SAAA,EAAW;AAAA,IACT,YAAA,EAAc,CAAC,mCAAmC,CAAA;AAAA,IAClD,aAAA,EAAe,CAAC,mCAAmC,CAAA;AAAA,IACnD,WAAA,EAAa,CAAC,mCAAmC;AAAA,EACnD,CAAA;AAAA,EACA,OAAA,EAAS;AAAA,IACP,SAAA,EAAW,CAAC,4BAA4B;AAAA,EAC1C,CAAA;AAAA,EACA,WAAA,EAAa;AAAA,IACX,WAAA,EAAa,CAAC,8BAA8B,CAAA;AAAA,IAC5C,UAAA,EAAY,CAAC,2BAA2B;AAAA,EAC1C;AACF,CAAA;AAEA,SAAS,gBAAA,CAAiB,GAAA,EAAkC;AAC1D,EAAA,OAAO,GAAA,CAAI,cAAA,EAAgB,GAAA,CAAI,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,IAAA,CAAK,CAAC,EAAA,EAAI,CAAC,CAAA;AAClF;AAFS,qCAAA,gBAAA,EAAA,kBAAA,CAAA;AAOF,SAAS,QAAA,CAAS,OAAA,EAAqB,GAAA,EAAgC;AAC5E,EAAA,MAAM,OAAA,EAAS,GAAA,CAAI,oBAAA,GAAuB,EAAA;AAC1C,EAAA,MAAM,aAAA,EAAe,gBAAA,CAAiB,GAAG,CAAA;AACzC,EAAA,MAAM,SAAA,mBAAW,OAAA,CAAQ,QAAA,UAAY,CAAC,GAAA;AAItC,EAAA,MAAM,gBAAA,EAA+E;AAAA;AAAA,IAEnF,YAAA,EAAc,EAAE,IAAA,EAAM,CAAC,QAAA,EAAU,iBAAA,EAAmB,eAAe,CAAA,EAAG,OAAA,EAAS,CAAC,EAAE,CAAA;AAAA;AAAA,IAElF,WAAA,EAAa,EAAE,IAAA,EAAM,CAAC,QAAA,EAAU,iBAAiB,CAAA,EAAG,OAAA,EAAS,CAAC,EAAE,CAAA;AAAA;AAAA,IAEhE,SAAA,EAAW,EAAE,IAAA,EAAM,CAAC,QAAA,EAAU,OAAA,EAAS,OAAO,CAAA,EAAG,OAAA,EAAS,aAAa,CAAA;AAAA;AAAA,IAEvE,UAAA,EAAY,EAAE,IAAA,EAAM,CAAC,QAAQ,CAAA,EAAG,OAAA,EAAS,CAAC,EAAE,CAAA;AAAA;AAAA,IAE5C,aAAA,EAAe;AAAA,MACb,IAAA,EAAM,CAAC,QAAQ,CAAA;AAAA,MACf,OAAA,EAAS;AAAA,QACP,MAAA;AAAA;AAAA,QAEA,MAAA,CAAO,OAAA,CAAQ,OAAA,EAAS,IAAI,CAAA;AAAA,QAC5B,GAAA,CAAI,oBAAA,GAAuB,EAAA;AAAA,QAC3B,GAAG;AAAA,MACL;AAAA,IACF,CAAA;AAAA;AAAA,IAEA,WAAA,EAAa,EAAE,IAAA,EAAM,CAAC,QAAQ,CAAA,EAAG,OAAA,EAAS,CAAC,EAAE,CAAA;AAAA;AAAA,IAE7C,YAAA,EAAc,EAAE,IAAA,EAAM,CAAC,QAAA,EAAU,OAAO,CAAA,EAAG,OAAA,EAAS,CAAC,EAAE;AAAA,EACzD,CAAA;AAEA,EAAA,MAAM,UAAA,kBAAY,qCAAA,CAAC,IAAA,EAAA,GAA+B;AAChD,IAAA,MAAM,EAAE,IAAA,EAAM,QAAQ,EAAA,EAAI,eAAA,CAAgB,IAAI,CAAA;AAC9C,IAAA,MAAM,aAAA,EAAe,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,EAAA,oBAAY,eAAA,CAAgB,OAAO,CAAA,CAAE,IAAI,CAAA,UAAK,CAAC,GAAC,CAAA;AACvF,IAAA,MAAM,QAAA,EAAU,CAAC,GAAG,IAAA,EAAM,GAAG,YAAA,EAAc,oCAAI,OAAA,mBAAQ,GAAA,4BAAA,CAAM,IAAI,GAAA,UAAK,CAAC,GAAA,EAAI,GAAG,OAAO,CAAA,CAAE,MAAA;AAAA,MACrF,CAAC,MAAA,EAAA,GAAW,OAAA,IAAW;AAAA,IACzB,CAAA;AACA,IAAA,OAAO,CAAC,IAAA,EAAM,GAAG,IAAI,GAAA,CAAI,OAAO,CAAC,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,EAC7C,CAAA,EAPkB,WAAA,CAAA;AASlB,EAAA,OAAO;AAAA,IACL,oBAAA;AAAA,IACA,SAAA,CAAU,YAAY,CAAA;AAAA,IACtB,SAAA,CAAU,WAAW,CAAA;AAAA,IACrB,SAAA,CAAU,SAAS,CAAA;AAAA,IACnB,SAAA,CAAU,UAAU,CAAA;AAAA,IACpB,SAAA,CAAU,aAAa,CAAA;AAAA,IACvB,SAAA,CAAU,WAAW,CAAA;AAAA;AAAA,IAErB,wBAAA;AAAA;AAAA,IAEA,oBAAA;AAAA;AAAA,IAEA,iBAAA;AAAA;AAAA,IAEA,mBAAA;AAAA,IACA,SAAA,CAAU,YAAY,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQtB,GAAA,CAAI,SAAA,IAAa,aAAA,GAAgB,GAAA,CAAI,UAAA,IAAc,OAAA,EAAS,4BAAA,EAA8B;AAAA,EAC5F,CAAA,CACG,MAAA,CAAO,OAAO,CAAA,CACd,IAAA,CAAK,IAAI,CAAA;AACd;AAtEgB,qCAAA,QAAA,EAAA,UAAA,CAAA;AD0ChB;AACA;AExFO,SAAS,oBAAA,CAAqB,OAAA,EAAgC,GAAA,EAAsC;AACzG,EAAA,MAAM,OAAA,kBAAS,OAAA,qBAAQ,iBAAA,6BAAmB,SAAA,IAAW,OAAA,EAAS,GAAA,EAAK,MAAA;AAEnE,EAAA,OAAO;AAAA,IACL;AAAA;AAAA,MAEE,MAAA,EAAQ,SAAA;AAAA,MACR,OAAA,EAAS;AAAA;AAAA,QAEP;AAAA,UACE,GAAA,EAAK,yBAAA;AAAA,UACL,KAAA,EAAO,QAAA,CAAS,OAAA,EAAS,GAAG;AAAA,QAC9B,CAAA;AAAA;AAAA,QAEA;AAAA,UACE,GAAA,EAAK,iBAAA;AAAA,UACL,KAAA,EAAO;AAAA,QACT,CAAA;AAAA;AAAA,QAEA;AAAA,UACE,GAAA,EAAK,wBAAA;AAAA,UACL,KAAA,EAAO;AAAA,QACT,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKA,GAAI,GAAA,CAAI,UAAA,IAAc,OAAA,EAClB,CAAC,EAAA,EACD;AAAA,UACE;AAAA,YACE,GAAA,EAAK,2BAAA;AAAA,YACL,KAAA,EAAO;AAAA,UACT;AAAA,QACF,CAAA;AAAA;AAAA,QAEJ;AAAA,UACE,GAAA,EAAK,iBAAA;AAAA,UACL,KAAA,EAAO;AAAA,QACT,CAAA;AAAA;AAAA,QAEA;AAAA,UACE,GAAA,EAAK,oBAAA;AAAA,UACL,KAAA,EAAO,CAAA,QAAA,EAAW,MAAM,CAAA,gCAAA;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AACF;AAhDgB,qCAAA,oBAAA,EAAA,sBAAA,CAAA;AFwIhB;AACA;AGnHA,SAAS,iBAAA,CAAkB,GAAA,EAAyC;AAClE,EAAA,OAAO,GAAA,CAAI,cAAA,EACP,GAAA,CAAI,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,GAAA,EAAA,GAAQ;AACxC,IAAA,MAAM,EAAE,QAAA,EAAU,QAAA,EAAU,KAAK,EAAA,EAAI,IAAI,GAAA,CAAI,GAAA,CAAI,IAAA,CAAK,CAAC,CAAA;AACvD,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,QAAA,CAAS,OAAA,CAAQ,GAAA,EAAK,EAAE,CAAA;AAAA,MAClC,QAAA;AAAA,MACA,IAAA,EAAM,KAAA,GAAQ,KAAA;AAAA,IAChB,CAAA;AAAA,EACF,CAAC,EAAA,EACD,CAAC,CAAA;AACP;AAXS,qCAAA,iBAAA,EAAA,mBAAA,CAAA;AAaF,SAAS,gBAAA,CAAiB,OAAA,EAA2C;AAC1E,EAAA,MAAM,IAAA,EAAM,OAAA,CAAQ,GAAA;AACpB,EAAA,MAAM,cAAA,EAAgB,OAAA,CAAQ,cAAA,IAAkB,IAAA;AAEhD,EAAA,MAAM,OAAA,EAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQzB,GAAI,GAAA,CAAI,UAAA,IAAc,OAAA,EAAS,EAAE,OAAA,EAAS,YAAY,EAAA,EAAI,CAAC,CAAA;AAAA,IAC3D,eAAA,EAAiB,IAAA;AAAA,IACjB,kBAAA,EAAoB,IAAA;AAAA,IACpB,aAAA;AAAA,IACA,YAAA,EAAc;AAAA,MACZ,aAAA,EAAe;AAAA,QACb,aAAA,EAAe;AAAA,MACjB,CAAA;AAAA;AAAA;AAAA;AAAA,MAIA,cAAA,mBAAgB,OAAA,CAAQ,cAAA,UAAkB,MAAA;AAAA,MAC1C,WAAA,EAAa,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKb,GAAI,cAAA,EAAgB,EAAE,0BAAA,EAA4B,KAAK,EAAA,EAAI,CAAC;AAAA,IAC9D,CAAA;AAAA,IACA,iBAAA,EAAmB,OAAA,CAAQ,UAAA;AAAA,IAC3B,cAAA,EAAgB,OAAA,CAAQ,IAAA,EAAM,CAAC,IAAA,EAAM,KAAA,EAAO,KAAK,EAAA,EAAI,CAAC,IAAA,EAAM,KAAK,CAAA;AAAA,IACjE,MAAA,EAAQ;AAAA,MACN,eAAA,mBAAiB,OAAA,CAAQ,oBAAA,UAAwB,IAAA;AAAA,MACjD,cAAA,EAAgB,iBAAA,CAAkB,GAAG,CAAA;AAAA,MACrC,uBAAA,EAAyB;AAAA,IAC3B,CAAA;AAAA,IACA,eAAA,EAAiB,KAAA;AAAA,IACjB,eAAA,EAAiB,KAAA;AAAA,IACjB,GAAA,EAAK;AAAA,MACH,mBAAA,mBAAqB,GAAA,CAAI,mBAAA,UAAuB,IAAA;AAAA,MAChD,mBAAA,mBAAqB,GAAA,CAAI,mBAAA,UAAuB,IAAA;AAAA,MAChD,4BAAA,mBAA8B,GAAA,CAAI,4BAAA,UAAgC,IAAA;AAAA,MAClE,+BAAA,mBAAiC,GAAA,CAAI,+BAAA,UAAmC,IAAA;AAAA,MACxE,GAAG,OAAA,CAAQ;AAAA,IACb,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,GAAI,OAAA,CAAQ,QAAA,EAAU,EAAE,sBAAA,EAAwB,CAAC,SAAA,EAAW,cAAc,EAAE,EAAA,EAAI,CAAC,CAAA;AAAA;AAAA;AAAA,IAGjF,SAAA,EAAW;AAAA,MACT,YAAA,EAAc;AAAA,QACZ,GAAA,EAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,GAAA,CAAI,OAAA,CAAQ,gBAAA,IAAoB,KAAA,EAAO;AACrC,IAAA,MAAA,CAAO,QAAA,EAAU,MAAA,CAAA,EAAA,GAAY,oBAAA,CAAqB,OAAA,EAAS,OAAA,CAAQ,GAAG,CAAA;AAAA,EACxE;AAEA,EAAA,GAAA,CAAI,OAAA,CAAQ,QAAA,EAAU,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,QAAA;AAEhD,EAAA,OAAO,OAAA,CAAQ,OAAA,EAAS,OAAA,CAAQ,MAAA,CAAO,MAAM,EAAA,EAAI,MAAA;AACnD;AArEgB,qCAAA,gBAAA,EAAA,kBAAA,CAAA;AHqLhB;AACE;AACA;AACF,yEAAC","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/next-config/index.js","sourcesContent":[null,"// Runtime imports: none. next.config.js loads this at RUNTIME under `next start`\n// in an image installed with --prod, so it may import only Node built-ins.\n\nexport type CspDirective =\n \"script-src\" | \"style-src\" | \"img-src\" | \"font-src\" | \"connect-src\" | \"frame-src\" | \"worker-src\";\n\n/**\n * Third-party services an app may load. Each one adds its hosts to the\n * directives it needs; nothing is allowed unless the app names it.\n *\n * - `stripe`: the billing feature (`@carlonicora/nextjs-jsonapi/billing`).\n * - `googleMaps`: `FormPlaceAutocomplete` (Places API) and Maps JS/static tiles.\n * - `turnstile`: Cloudflare Turnstile widgets (contact and waitlist forms).\n * - `discord`: Discord avatars shown for users who log in with Discord.\n * - `googleFonts`: fonts linked at runtime from Google Fonts. Not needed for\n * `next/font/google`, which self-hosts at build time.\n */\nexport type CspService = \"stripe\" | \"googleMaps\" | \"turnstile\" | \"discord\" | \"googleFonts\";\n\nexport interface CspOptions {\n services?: CspService[];\n csp?: Partial<Record<CspDirective, string[]>>;\n}\n\nconst SERVICE_SOURCES: Record<CspService, Partial<Record<CspDirective, string[]>>> = {\n stripe: {\n \"script-src\": [\"https://js.stripe.com\"],\n \"img-src\": [\"https://*.stripe.com\"],\n \"connect-src\": [\"https://api.stripe.com\"],\n \"frame-src\": [\"https://js.stripe.com\", \"https://hooks.stripe.com\"],\n },\n googleMaps: {\n \"script-src\": [\"https://maps.googleapis.com\"],\n \"img-src\": [\"https://maps.googleapis.com\", \"https://maps.gstatic.com\"],\n \"connect-src\": [\"https://maps.googleapis.com\", \"https://places.googleapis.com\"],\n },\n turnstile: {\n \"script-src\": [\"https://challenges.cloudflare.com\"],\n \"connect-src\": [\"https://challenges.cloudflare.com\"],\n \"frame-src\": [\"https://challenges.cloudflare.com\"],\n },\n discord: {\n \"img-src\": [\"https://cdn.discordapp.com\"],\n },\n googleFonts: {\n \"style-src\": [\"https://fonts.googleapis.com\"],\n \"font-src\": [\"https://fonts.gstatic.com\"],\n },\n};\n\nfunction listImageSources(env: NodeJS.ProcessEnv): string[] {\n return env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((s) => s.trim()) : [];\n}\n\n// Build CSP directives\n// Note: 'unsafe-inline' and 'unsafe-eval' needed for Next.js dev mode and some runtime features\n// In production, consider using nonces for stricter CSP\nexport function buildCsp(options: CspOptions, env: NodeJS.ProcessEnv): string {\n const apiUrl = env.NEXT_PUBLIC_API_URL || \"\";\n const imageSources = listImageSources(env);\n const services = options.services ?? [];\n\n // Each fetch directive: what Next itself needs, then the named services, then\n // the app's extras, then the sources derived from the env.\n const fetchDirectives: Record<CspDirective, { base: string[]; fromEnv: string[] }> = {\n // Scripts: self, and unsafe-inline/eval for Next.js\n \"script-src\": { base: [\"'self'\", \"'unsafe-inline'\", \"'unsafe-eval'\"], fromEnv: [] },\n // Styles: self and unsafe-inline for styled-components/emotion\n \"style-src\": { base: [\"'self'\", \"'unsafe-inline'\"], fromEnv: [] },\n // Images: self, data URIs, blobs, and configured image sources\n \"img-src\": { base: [\"'self'\", \"data:\", \"blob:\"], fromEnv: imageSources },\n // Fonts: self\n \"font-src\": { base: [\"'self'\"], fromEnv: [] },\n // Connect: self, API (http + ws for socket.io), and storage (for uploads)\n \"connect-src\": {\n base: [\"'self'\"],\n fromEnv: [\n apiUrl,\n // Add WebSocket URL for socket.io (convert http->ws, https->wss)\n apiUrl.replace(/^http/, \"ws\"),\n env.NEXT_PUBLIC_ADDRESS || \"\",\n ...imageSources,\n ],\n },\n // Frames: self\n \"frame-src\": { base: [\"'self'\"], fromEnv: [] },\n // Worker sources: self and blob (for Web Workers with dynamic imports)\n \"worker-src\": { base: [\"'self'\", \"blob:\"], fromEnv: [] },\n };\n\n const directive = (name: CspDirective): string => {\n const { base, fromEnv } = fetchDirectives[name];\n const fromServices = services.flatMap((service) => SERVICE_SOURCES[service][name] ?? []);\n const sources = [...base, ...fromServices, ...(options.csp?.[name] ?? []), ...fromEnv].filter(\n (source) => source !== \"\",\n );\n return [name, ...new Set(sources)].join(\" \");\n };\n\n return [\n \"default-src 'self'\",\n directive(\"script-src\"),\n directive(\"style-src\"),\n directive(\"img-src\"),\n directive(\"font-src\"),\n directive(\"connect-src\"),\n directive(\"frame-src\"),\n // Frame ancestors: deny embedding (equivalent to X-Frame-Options: DENY)\n \"frame-ancestors 'none'\",\n // Form actions: only self\n \"form-action 'self'\",\n // Base URI: only self\n \"base-uri 'self'\",\n // Object sources: none (no plugins)\n \"object-src 'none'\",\n directive(\"worker-src\"),\n // Upgrade insecure requests in production.\n //\n // NOT under E2E_BUILD: the e2e harness runs a PRODUCTION build (for speed)\n // and serves it over plain http on a custom hostname. With this directive\n // the browser rewrites every /_next/static asset to https, which nothing is\n // listening on — every chunk fails with ERR_SSL_PROTOCOL_ERROR, the page\n // never hydrates, and each spec sees only server-rendered markup.\n env.NODE_ENV === \"production\" && env.E2E_BUILD !== \"true\" ? \"upgrade-insecure-requests\" : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n}\n","// Runtime imports: relative only. See csp.ts.\nimport { buildCsp, type CspOptions } from \"./csp\";\n\nexport interface SecurityHeaderOptions extends CspOptions {\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n}\n\nexport interface HeaderRule {\n source: string;\n headers: { key: string; value: string }[];\n}\n\nexport function buildSecurityHeaders(options: SecurityHeaderOptions, env: NodeJS.ProcessEnv): HeaderRule[] {\n const camera = options.permissionsPolicy?.camera === \"none\" ? \"\" : \"self\";\n\n return [\n {\n // Apply to all routes\n source: \"/:path*\",\n headers: [\n // Content Security Policy - prevents XSS attacks\n {\n key: \"Content-Security-Policy\",\n value: buildCsp(options, env),\n },\n // X-Frame-Options - prevents clickjacking (backup for older browsers)\n {\n key: \"X-Frame-Options\",\n value: \"DENY\",\n },\n // X-Content-Type-Options - prevents MIME sniffing\n {\n key: \"X-Content-Type-Options\",\n value: \"nosniff\",\n },\n // Strict-Transport-Security - enforces HTTPS (1 year, include subdomains).\n // Omitted under E2E_BUILD for the same reason as upgrade-insecure-requests:\n // the suite speaks plain http, and an HSTS pin would force https on every\n // later request to this host from the same browser profile.\n ...(env.E2E_BUILD === \"true\"\n ? []\n : [\n {\n key: \"Strict-Transport-Security\",\n value: \"max-age=31536000; includeSubDomains\",\n },\n ]),\n // Referrer-Policy - controls referrer information\n {\n key: \"Referrer-Policy\",\n value: \"strict-origin-when-cross-origin\",\n },\n // Permissions-Policy - restricts browser features\n {\n key: \"Permissions-Policy\",\n value: `camera=(${camera}), microphone=(), geolocation=()`,\n },\n ],\n },\n ];\n}\n","// Shared next.config for the nja fleet. Apps wrap the result with their own\n// plugins: `withNextIntl(withMDX(createNextConfig({...})))`.\n//\n// next.config.js is loaded at RUNTIME by `next start`, and the production image\n// installs with --prod. This entry may therefore import only Node built-ins at\n// runtime: never next-intl, @next/mdx, serwist or any devDependency. `next` is\n// imported for its types only.\nimport type { NextConfig } from \"next\";\nimport { buildCsp, type CspDirective, type CspService } from \"./csp\";\nimport { buildSecurityHeaders } from \"./headers\";\n\nexport { buildCsp };\nexport type { CspDirective, CspService };\n\nexport interface NjaNextConfigOptions {\n devOrigins: string[];\n /** Third-party services the app loads; each adds its own hosts to the CSP. Nothing is allowed by default. */\n services?: CspService[];\n /** Extra CSP sources per directive, for hosts no named service covers. */\n csp?: Partial<Record<CspDirective, string[]>>;\n env?: Record<string, string>;\n imageMinimumCacheTTL?: number;\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n mdx?: boolean;\n serwist?: boolean;\n securityHeaders?: boolean;\n authInterrupts?: boolean;\n reactCompiler?: boolean;\n rewrites?: NextConfig[\"rewrites\"];\n extend?: (config: NextConfig) => NextConfig;\n}\n\ntype RemotePattern = { protocol: \"http\" | \"https\"; hostname: string; port?: string };\n\nfunction parseImageSources(env: NodeJS.ProcessEnv): RemotePattern[] {\n return env.IMAGE_SOURCES\n ? env.IMAGE_SOURCES.split(\",\").map((url) => {\n const { hostname, protocol, port } = new URL(url.trim());\n return {\n protocol: protocol.replace(\":\", \"\") as RemotePattern[\"protocol\"],\n hostname,\n port: port || undefined,\n };\n })\n : [];\n}\n\nexport function createNextConfig(options: NjaNextConfigOptions): NextConfig {\n const env = process.env;\n const reactCompiler = options.reactCompiler === true;\n\n const config: NextConfig = {\n // The e2e stack (scripts/e2e.sh) runs a PRODUCTION build of this app while a\n // `next dev` server may still be running against the same worktree. `next\n // build` and `next dev` share `.next`, so a build performed underneath a live\n // dev server clobbers it and that dev server then 404s every route. Giving\n // the e2e build its own directory keeps the two independent. E2E_BUILD must\n // be set for `next build` AND for `next start`, or `start` looks in `.next`\n // and serves someone else's output.\n ...(env.E2E_BUILD === \"true\" ? { distDir: \".next-e2e\" } : {}),\n cacheComponents: true,\n partialPrefetching: true,\n reactCompiler,\n experimental: {\n serverActions: {\n bodySizeLimit: \"5mb\",\n },\n // Required by `unauthorized()` (next/navigation): without this flag it throws a plain\n // Error instead of rendering the nearest unauthorized.tsx. The flag is for app code that\n // calls it; ServerSession.checkPermission does not, it still does `redirect(\"/401\")`.\n authInterrupts: options.authInterrupts ?? true,\n turbopackGc: true,\n // No turbopackLazyDynamicImports: in `next dev` it leaves this package's lazily\n // imported request module unactivated (\"Lazily compiled dynamic import of ...\n // JsonApiRequest ... was not activated\"), so every client-side read returns\n // nothing. Seen with the package installed from npm; production builds are unaffected.\n ...(reactCompiler ? { turbopackRustReactCompiler: true } : {}),\n },\n allowedDevOrigins: options.devOrigins,\n pageExtensions: options.mdx ? [\"ts\", \"tsx\", \"mdx\"] : [\"ts\", \"tsx\"],\n images: {\n minimumCacheTTL: options.imageMinimumCacheTTL ?? 60,\n remotePatterns: parseImageSources(env),\n dangerouslyAllowLocalIP: true,\n },\n poweredByHeader: false,\n reactStrictMode: false,\n env: {\n NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL ?? \"\",\n NEXT_PUBLIC_ADDRESS: env.NEXT_PUBLIC_ADDRESS ?? \"\",\n NEXT_PUBLIC_VAPID_PUBLIC_KEY: env.NEXT_PUBLIC_VAPID_PUBLIC_KEY ?? \"\",\n NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? \"\",\n ...options.env,\n },\n // Inlined replacement for @serwist/turbopack's withSerwist wrapper, which\n // does nothing but append these two entries. Requiring the package here\n // instead CRASHES production: it is a devDependency, next.config.js is loaded\n // at RUNTIME by `next start`, and the web-production image installs with\n // --prod. Do not re-add a require of any devDependency to this file.\n ...(options.serwist ? { serverExternalPackages: [\"esbuild\", \"esbuild-wasm\"] } : {}),\n // Next 16 builds AND serves dev with Turbopack, so this is what pins yjs to a\n // single instance in both modes.\n turbopack: {\n resolveAlias: {\n yjs: \"./node_modules/yjs\",\n },\n },\n };\n\n if (options.securityHeaders !== false) {\n config.headers = async () => buildSecurityHeaders(options, process.env);\n }\n\n if (options.rewrites) config.rewrites = options.rewrites;\n\n return options.extend ? options.extend(config) : config;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/next-config/index.js","../../src/next-config/csp.ts","../../src/next-config/headers.ts","../../src/next-config/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/next-config/index.js","sourcesContent":[null,"// Runtime imports: none. next.config.js loads this at RUNTIME under `next start`\n// in an image installed with --prod, so it may import only Node built-ins.\n\nexport type CspDirective =\n \"script-src\" | \"style-src\" | \"img-src\" | \"font-src\" | \"connect-src\" | \"frame-src\" | \"worker-src\";\n\n/**\n * Third-party services an app may load. Each one adds its hosts to the\n * directives it needs; nothing is allowed unless the app names it.\n *\n * - `stripe`: the billing feature (`@carlonicora/nextjs-jsonapi/billing`).\n * - `googleMaps`: `FormPlaceAutocomplete` (Places API) and Maps JS/static tiles.\n * - `turnstile`: Cloudflare Turnstile widgets (contact and waitlist forms).\n * - `discord`: Discord avatars shown for users who log in with Discord.\n * - `googleFonts`: fonts linked at runtime from Google Fonts. Not needed for\n * `next/font/google`, which self-hosts at build time.\n */\nexport type CspService = \"stripe\" | \"googleMaps\" | \"turnstile\" | \"discord\" | \"googleFonts\";\n\nexport interface CspOptions {\n services?: CspService[];\n csp?: Partial<Record<CspDirective, string[]>>;\n}\n\nconst SERVICE_SOURCES: Record<CspService, Partial<Record<CspDirective, string[]>>> = {\n stripe: {\n \"script-src\": [\"https://js.stripe.com\"],\n \"img-src\": [\"https://*.stripe.com\"],\n \"connect-src\": [\"https://api.stripe.com\"],\n \"frame-src\": [\"https://js.stripe.com\", \"https://hooks.stripe.com\"],\n },\n googleMaps: {\n \"script-src\": [\"https://maps.googleapis.com\"],\n \"img-src\": [\"https://maps.googleapis.com\", \"https://maps.gstatic.com\"],\n \"connect-src\": [\"https://maps.googleapis.com\", \"https://places.googleapis.com\"],\n },\n turnstile: {\n \"script-src\": [\"https://challenges.cloudflare.com\"],\n \"connect-src\": [\"https://challenges.cloudflare.com\"],\n \"frame-src\": [\"https://challenges.cloudflare.com\"],\n },\n discord: {\n \"img-src\": [\"https://cdn.discordapp.com\"],\n },\n googleFonts: {\n \"style-src\": [\"https://fonts.googleapis.com\"],\n \"font-src\": [\"https://fonts.gstatic.com\"],\n },\n};\n\nfunction listImageSources(env: NodeJS.ProcessEnv): string[] {\n return env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((s) => s.trim()) : [];\n}\n\n/**\n * Origins of the app's own servers, the API and the corpus, from\n * `NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_CORPUS_URL`. They serve every stored\n * object link, so images and frames must be allowed from them. Unset or invalid\n * URLs are skipped; the result has no duplicates.\n */\nexport function listAppOrigins(env: NodeJS.ProcessEnv): string[] {\n const origins: string[] = [];\n for (const url of [env.NEXT_PUBLIC_API_URL, env.NEXT_PUBLIC_CORPUS_URL]) {\n if (!url) continue;\n try {\n const { origin } = new URL(url.trim());\n if (origin !== \"null\" && !origins.includes(origin)) origins.push(origin);\n } catch {\n // Not a URL: nothing to allow.\n }\n }\n return origins;\n}\n\n// Build CSP directives\n// Note: 'unsafe-inline' and 'unsafe-eval' needed for Next.js dev mode and some runtime features\n// In production, consider using nonces for stricter CSP\nexport function buildCsp(options: CspOptions, env: NodeJS.ProcessEnv): string {\n const apiUrl = env.NEXT_PUBLIC_API_URL || \"\";\n const imageSources = listImageSources(env);\n const appOrigins = listAppOrigins(env);\n const services = options.services ?? [];\n\n // Each fetch directive: what Next itself needs, then the named services, then\n // the app's extras, then the sources derived from the env.\n const fetchDirectives: Record<CspDirective, { base: string[]; fromEnv: string[] }> = {\n // Scripts: self, and unsafe-inline/eval for Next.js\n \"script-src\": { base: [\"'self'\", \"'unsafe-inline'\", \"'unsafe-eval'\"], fromEnv: [] },\n // Styles: self and unsafe-inline for styled-components/emotion\n \"style-src\": { base: [\"'self'\", \"'unsafe-inline'\"], fromEnv: [] },\n // Images: self, data URIs, blobs, configured image sources, and the api/corpus origins\n \"img-src\": { base: [\"'self'\", \"data:\", \"blob:\"], fromEnv: [...imageSources, ...appOrigins] },\n // Fonts: self\n \"font-src\": { base: [\"'self'\"], fromEnv: [] },\n // Connect: self, API (http + ws for socket.io), and storage (for uploads)\n \"connect-src\": {\n base: [\"'self'\"],\n fromEnv: [\n apiUrl,\n // Add WebSocket URL for socket.io (convert http->ws, https->wss)\n apiUrl.replace(/^http/, \"ws\"),\n env.NEXT_PUBLIC_ADDRESS || \"\",\n ...imageSources,\n ],\n },\n // Frames: self, and the api/corpus origins (inline previews of stored objects)\n \"frame-src\": { base: [\"'self'\"], fromEnv: appOrigins },\n // Worker sources: self and blob (for Web Workers with dynamic imports)\n \"worker-src\": { base: [\"'self'\", \"blob:\"], fromEnv: [] },\n };\n\n const directive = (name: CspDirective): string => {\n const { base, fromEnv } = fetchDirectives[name];\n const fromServices = services.flatMap((service) => SERVICE_SOURCES[service][name] ?? []);\n const sources = [...base, ...fromServices, ...(options.csp?.[name] ?? []), ...fromEnv].filter(\n (source) => source !== \"\",\n );\n return [name, ...new Set(sources)].join(\" \");\n };\n\n return [\n \"default-src 'self'\",\n directive(\"script-src\"),\n directive(\"style-src\"),\n directive(\"img-src\"),\n directive(\"font-src\"),\n directive(\"connect-src\"),\n directive(\"frame-src\"),\n // Frame ancestors: deny embedding (equivalent to X-Frame-Options: DENY)\n \"frame-ancestors 'none'\",\n // Form actions: only self\n \"form-action 'self'\",\n // Base URI: only self\n \"base-uri 'self'\",\n // Object sources: none (no plugins)\n \"object-src 'none'\",\n directive(\"worker-src\"),\n // Upgrade insecure requests in production.\n //\n // NOT under E2E_BUILD: the e2e harness runs a PRODUCTION build (for speed)\n // and serves it over plain http on a custom hostname. With this directive\n // the browser rewrites every /_next/static asset to https, which nothing is\n // listening on — every chunk fails with ERR_SSL_PROTOCOL_ERROR, the page\n // never hydrates, and each spec sees only server-rendered markup.\n env.NODE_ENV === \"production\" && env.E2E_BUILD !== \"true\" ? \"upgrade-insecure-requests\" : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n}\n","// Runtime imports: relative only. See csp.ts.\nimport { buildCsp, type CspOptions } from \"./csp\";\n\nexport interface SecurityHeaderOptions extends CspOptions {\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n}\n\nexport interface HeaderRule {\n source: string;\n headers: { key: string; value: string }[];\n}\n\nexport function buildSecurityHeaders(options: SecurityHeaderOptions, env: NodeJS.ProcessEnv): HeaderRule[] {\n const camera = options.permissionsPolicy?.camera === \"none\" ? \"\" : \"self\";\n\n return [\n {\n // Apply to all routes\n source: \"/:path*\",\n headers: [\n // Content Security Policy - prevents XSS attacks\n {\n key: \"Content-Security-Policy\",\n value: buildCsp(options, env),\n },\n // X-Frame-Options - prevents clickjacking (backup for older browsers)\n {\n key: \"X-Frame-Options\",\n value: \"DENY\",\n },\n // X-Content-Type-Options - prevents MIME sniffing\n {\n key: \"X-Content-Type-Options\",\n value: \"nosniff\",\n },\n // Strict-Transport-Security - enforces HTTPS (1 year, include subdomains).\n // Omitted under E2E_BUILD for the same reason as upgrade-insecure-requests:\n // the suite speaks plain http, and an HSTS pin would force https on every\n // later request to this host from the same browser profile.\n ...(env.E2E_BUILD === \"true\"\n ? []\n : [\n {\n key: \"Strict-Transport-Security\",\n value: \"max-age=31536000; includeSubDomains\",\n },\n ]),\n // Referrer-Policy - controls referrer information\n {\n key: \"Referrer-Policy\",\n value: \"strict-origin-when-cross-origin\",\n },\n // Permissions-Policy - restricts browser features\n {\n key: \"Permissions-Policy\",\n value: `camera=(${camera}), microphone=(), geolocation=()`,\n },\n ],\n },\n ];\n}\n","// Shared next.config for the nja fleet. Apps wrap the result with their own\n// plugins: `withNextIntl(withMDX(createNextConfig({...})))`.\n//\n// next.config.js is loaded at RUNTIME by `next start`, and the production image\n// installs with --prod. This entry may therefore import only Node built-ins at\n// runtime: never next-intl, @next/mdx, serwist or any devDependency. `next` is\n// imported for its types only.\nimport type { NextConfig } from \"next\";\nimport { buildCsp, listAppOrigins, type CspDirective, type CspService } from \"./csp\";\nimport { buildSecurityHeaders } from \"./headers\";\n\nexport { buildCsp, listAppOrigins };\nexport type { CspDirective, CspService };\n\nexport interface NjaNextConfigOptions {\n devOrigins: string[];\n /** Third-party services the app loads; each adds its own hosts to the CSP. Nothing is allowed by default. */\n services?: CspService[];\n /** Extra CSP sources per directive, for hosts no named service covers. */\n csp?: Partial<Record<CspDirective, string[]>>;\n env?: Record<string, string>;\n imageMinimumCacheTTL?: number;\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n mdx?: boolean;\n serwist?: boolean;\n securityHeaders?: boolean;\n authInterrupts?: boolean;\n reactCompiler?: boolean;\n rewrites?: NextConfig[\"rewrites\"];\n extend?: (config: NextConfig) => NextConfig;\n}\n\ntype RemotePattern = { protocol: \"http\" | \"https\"; hostname: string; port?: string };\n\n// IMAGE_SOURCES, then the api and corpus origins (they serve every stored object\n// link), without duplicates.\nfunction parseImageSources(env: NodeJS.ProcessEnv): RemotePattern[] {\n const imageSources = env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((url) => url.trim()) : [];\n const patterns: RemotePattern[] = [];\n for (const url of [...imageSources, ...listAppOrigins(env)]) {\n const { hostname, protocol, port } = new URL(url);\n const pattern: RemotePattern = {\n protocol: protocol.replace(\":\", \"\") as RemotePattern[\"protocol\"],\n hostname,\n port: port || undefined,\n };\n const seen = patterns.some(\n (p) => p.protocol === pattern.protocol && p.hostname === pattern.hostname && p.port === pattern.port,\n );\n if (!seen) patterns.push(pattern);\n }\n return patterns;\n}\n\nexport function createNextConfig(options: NjaNextConfigOptions): NextConfig {\n const env = process.env;\n const reactCompiler = options.reactCompiler === true;\n\n const config: NextConfig = {\n // The e2e stack (scripts/e2e.sh) runs a PRODUCTION build of this app while a\n // `next dev` server may still be running against the same worktree. `next\n // build` and `next dev` share `.next`, so a build performed underneath a live\n // dev server clobbers it and that dev server then 404s every route. Giving\n // the e2e build its own directory keeps the two independent. E2E_BUILD must\n // be set for `next build` AND for `next start`, or `start` looks in `.next`\n // and serves someone else's output.\n ...(env.E2E_BUILD === \"true\" ? { distDir: \".next-e2e\" } : {}),\n cacheComponents: true,\n partialPrefetching: true,\n reactCompiler,\n experimental: {\n serverActions: {\n bodySizeLimit: \"5mb\",\n },\n // Required by `unauthorized()` (next/navigation): without this flag it throws a plain\n // Error instead of rendering the nearest unauthorized.tsx. The flag is for app code that\n // calls it; ServerSession.checkPermission does not, it still does `redirect(\"/401\")`.\n authInterrupts: options.authInterrupts ?? true,\n turbopackGc: true,\n // No turbopackLazyDynamicImports: in `next dev` it leaves this package's lazily\n // imported request module unactivated (\"Lazily compiled dynamic import of ...\n // JsonApiRequest ... was not activated\"), so every client-side read returns\n // nothing. Seen with the package installed from npm; production builds are unaffected.\n ...(reactCompiler ? { turbopackRustReactCompiler: true } : {}),\n },\n allowedDevOrigins: options.devOrigins,\n pageExtensions: options.mdx ? [\"ts\", \"tsx\", \"mdx\"] : [\"ts\", \"tsx\"],\n images: {\n minimumCacheTTL: options.imageMinimumCacheTTL ?? 60,\n remotePatterns: parseImageSources(env),\n dangerouslyAllowLocalIP: true,\n },\n poweredByHeader: false,\n reactStrictMode: false,\n env: {\n NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL ?? \"\",\n NEXT_PUBLIC_ADDRESS: env.NEXT_PUBLIC_ADDRESS ?? \"\",\n NEXT_PUBLIC_VAPID_PUBLIC_KEY: env.NEXT_PUBLIC_VAPID_PUBLIC_KEY ?? \"\",\n NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? \"\",\n ...options.env,\n },\n // Inlined replacement for @serwist/turbopack's withSerwist wrapper, which\n // does nothing but append these two entries. Requiring the package here\n // instead CRASHES production: it is a devDependency, next.config.js is loaded\n // at RUNTIME by `next start`, and the web-production image installs with\n // --prod. Do not re-add a require of any devDependency to this file.\n ...(options.serwist ? { serverExternalPackages: [\"esbuild\", \"esbuild-wasm\"] } : {}),\n // Next 16 builds AND serves dev with Turbopack, so this is what pins yjs to a\n // single instance in both modes.\n turbopack: {\n resolveAlias: {\n yjs: \"./node_modules/yjs\",\n },\n },\n };\n\n if (options.securityHeaders !== false) {\n config.headers = async () => buildSecurityHeaders(options, process.env);\n }\n\n if (options.rewrites) config.rewrites = options.rewrites;\n\n return options.extend ? options.extend(config) : config;\n}\n"]}
|
|
@@ -32,17 +32,31 @@ function listImageSources(env) {
|
|
|
32
32
|
return env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(",").map((s) => s.trim()) : [];
|
|
33
33
|
}
|
|
34
34
|
__name(listImageSources, "listImageSources");
|
|
35
|
+
function listAppOrigins(env) {
|
|
36
|
+
const origins = [];
|
|
37
|
+
for (const url of [env.NEXT_PUBLIC_API_URL, env.NEXT_PUBLIC_CORPUS_URL]) {
|
|
38
|
+
if (!url) continue;
|
|
39
|
+
try {
|
|
40
|
+
const { origin } = new URL(url.trim());
|
|
41
|
+
if (origin !== "null" && !origins.includes(origin)) origins.push(origin);
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return origins;
|
|
46
|
+
}
|
|
47
|
+
__name(listAppOrigins, "listAppOrigins");
|
|
35
48
|
function buildCsp(options, env) {
|
|
36
49
|
const apiUrl = env.NEXT_PUBLIC_API_URL || "";
|
|
37
50
|
const imageSources = listImageSources(env);
|
|
51
|
+
const appOrigins = listAppOrigins(env);
|
|
38
52
|
const services = options.services ?? [];
|
|
39
53
|
const fetchDirectives = {
|
|
40
54
|
// Scripts: self, and unsafe-inline/eval for Next.js
|
|
41
55
|
"script-src": { base: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], fromEnv: [] },
|
|
42
56
|
// Styles: self and unsafe-inline for styled-components/emotion
|
|
43
57
|
"style-src": { base: ["'self'", "'unsafe-inline'"], fromEnv: [] },
|
|
44
|
-
// Images: self, data URIs, blobs,
|
|
45
|
-
"img-src": { base: ["'self'", "data:", "blob:"], fromEnv: imageSources },
|
|
58
|
+
// Images: self, data URIs, blobs, configured image sources, and the api/corpus origins
|
|
59
|
+
"img-src": { base: ["'self'", "data:", "blob:"], fromEnv: [...imageSources, ...appOrigins] },
|
|
46
60
|
// Fonts: self
|
|
47
61
|
"font-src": { base: ["'self'"], fromEnv: [] },
|
|
48
62
|
// Connect: self, API (http + ws for socket.io), and storage (for uploads)
|
|
@@ -56,8 +70,8 @@ function buildCsp(options, env) {
|
|
|
56
70
|
...imageSources
|
|
57
71
|
]
|
|
58
72
|
},
|
|
59
|
-
// Frames: self
|
|
60
|
-
"frame-src": { base: ["'self'"], fromEnv:
|
|
73
|
+
// Frames: self, and the api/corpus origins (inline previews of stored objects)
|
|
74
|
+
"frame-src": { base: ["'self'"], fromEnv: appOrigins },
|
|
61
75
|
// Worker sources: self and blob (for Web Workers with dynamic imports)
|
|
62
76
|
"worker-src": { base: ["'self'", "blob:"], fromEnv: [] }
|
|
63
77
|
};
|
|
@@ -149,14 +163,21 @@ __name(buildSecurityHeaders, "buildSecurityHeaders");
|
|
|
149
163
|
|
|
150
164
|
// src/next-config/index.ts
|
|
151
165
|
function parseImageSources(env) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
const imageSources = env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(",").map((url) => url.trim()) : [];
|
|
167
|
+
const patterns = [];
|
|
168
|
+
for (const url of [...imageSources, ...listAppOrigins(env)]) {
|
|
169
|
+
const { hostname, protocol, port } = new URL(url);
|
|
170
|
+
const pattern = {
|
|
155
171
|
protocol: protocol.replace(":", ""),
|
|
156
172
|
hostname,
|
|
157
173
|
port: port || void 0
|
|
158
174
|
};
|
|
159
|
-
|
|
175
|
+
const seen = patterns.some(
|
|
176
|
+
(p) => p.protocol === pattern.protocol && p.hostname === pattern.hostname && p.port === pattern.port
|
|
177
|
+
);
|
|
178
|
+
if (!seen) patterns.push(pattern);
|
|
179
|
+
}
|
|
180
|
+
return patterns;
|
|
160
181
|
}
|
|
161
182
|
__name(parseImageSources, "parseImageSources");
|
|
162
183
|
function createNextConfig(options) {
|
|
@@ -228,6 +249,7 @@ function createNextConfig(options) {
|
|
|
228
249
|
__name(createNextConfig, "createNextConfig");
|
|
229
250
|
export {
|
|
230
251
|
buildCsp,
|
|
231
|
-
createNextConfig
|
|
252
|
+
createNextConfig,
|
|
253
|
+
listAppOrigins
|
|
232
254
|
};
|
|
233
255
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/next-config/csp.ts","../../src/next-config/headers.ts","../../src/next-config/index.ts"],"sourcesContent":["// Runtime imports: none. next.config.js loads this at RUNTIME under `next start`\n// in an image installed with --prod, so it may import only Node built-ins.\n\nexport type CspDirective =\n \"script-src\" | \"style-src\" | \"img-src\" | \"font-src\" | \"connect-src\" | \"frame-src\" | \"worker-src\";\n\n/**\n * Third-party services an app may load. Each one adds its hosts to the\n * directives it needs; nothing is allowed unless the app names it.\n *\n * - `stripe`: the billing feature (`@carlonicora/nextjs-jsonapi/billing`).\n * - `googleMaps`: `FormPlaceAutocomplete` (Places API) and Maps JS/static tiles.\n * - `turnstile`: Cloudflare Turnstile widgets (contact and waitlist forms).\n * - `discord`: Discord avatars shown for users who log in with Discord.\n * - `googleFonts`: fonts linked at runtime from Google Fonts. Not needed for\n * `next/font/google`, which self-hosts at build time.\n */\nexport type CspService = \"stripe\" | \"googleMaps\" | \"turnstile\" | \"discord\" | \"googleFonts\";\n\nexport interface CspOptions {\n services?: CspService[];\n csp?: Partial<Record<CspDirective, string[]>>;\n}\n\nconst SERVICE_SOURCES: Record<CspService, Partial<Record<CspDirective, string[]>>> = {\n stripe: {\n \"script-src\": [\"https://js.stripe.com\"],\n \"img-src\": [\"https://*.stripe.com\"],\n \"connect-src\": [\"https://api.stripe.com\"],\n \"frame-src\": [\"https://js.stripe.com\", \"https://hooks.stripe.com\"],\n },\n googleMaps: {\n \"script-src\": [\"https://maps.googleapis.com\"],\n \"img-src\": [\"https://maps.googleapis.com\", \"https://maps.gstatic.com\"],\n \"connect-src\": [\"https://maps.googleapis.com\", \"https://places.googleapis.com\"],\n },\n turnstile: {\n \"script-src\": [\"https://challenges.cloudflare.com\"],\n \"connect-src\": [\"https://challenges.cloudflare.com\"],\n \"frame-src\": [\"https://challenges.cloudflare.com\"],\n },\n discord: {\n \"img-src\": [\"https://cdn.discordapp.com\"],\n },\n googleFonts: {\n \"style-src\": [\"https://fonts.googleapis.com\"],\n \"font-src\": [\"https://fonts.gstatic.com\"],\n },\n};\n\nfunction listImageSources(env: NodeJS.ProcessEnv): string[] {\n return env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((s) => s.trim()) : [];\n}\n\n// Build CSP directives\n// Note: 'unsafe-inline' and 'unsafe-eval' needed for Next.js dev mode and some runtime features\n// In production, consider using nonces for stricter CSP\nexport function buildCsp(options: CspOptions, env: NodeJS.ProcessEnv): string {\n const apiUrl = env.NEXT_PUBLIC_API_URL || \"\";\n const imageSources = listImageSources(env);\n const services = options.services ?? [];\n\n // Each fetch directive: what Next itself needs, then the named services, then\n // the app's extras, then the sources derived from the env.\n const fetchDirectives: Record<CspDirective, { base: string[]; fromEnv: string[] }> = {\n // Scripts: self, and unsafe-inline/eval for Next.js\n \"script-src\": { base: [\"'self'\", \"'unsafe-inline'\", \"'unsafe-eval'\"], fromEnv: [] },\n // Styles: self and unsafe-inline for styled-components/emotion\n \"style-src\": { base: [\"'self'\", \"'unsafe-inline'\"], fromEnv: [] },\n // Images: self, data URIs, blobs, and configured image sources\n \"img-src\": { base: [\"'self'\", \"data:\", \"blob:\"], fromEnv: imageSources },\n // Fonts: self\n \"font-src\": { base: [\"'self'\"], fromEnv: [] },\n // Connect: self, API (http + ws for socket.io), and storage (for uploads)\n \"connect-src\": {\n base: [\"'self'\"],\n fromEnv: [\n apiUrl,\n // Add WebSocket URL for socket.io (convert http->ws, https->wss)\n apiUrl.replace(/^http/, \"ws\"),\n env.NEXT_PUBLIC_ADDRESS || \"\",\n ...imageSources,\n ],\n },\n // Frames: self\n \"frame-src\": { base: [\"'self'\"], fromEnv: [] },\n // Worker sources: self and blob (for Web Workers with dynamic imports)\n \"worker-src\": { base: [\"'self'\", \"blob:\"], fromEnv: [] },\n };\n\n const directive = (name: CspDirective): string => {\n const { base, fromEnv } = fetchDirectives[name];\n const fromServices = services.flatMap((service) => SERVICE_SOURCES[service][name] ?? []);\n const sources = [...base, ...fromServices, ...(options.csp?.[name] ?? []), ...fromEnv].filter(\n (source) => source !== \"\",\n );\n return [name, ...new Set(sources)].join(\" \");\n };\n\n return [\n \"default-src 'self'\",\n directive(\"script-src\"),\n directive(\"style-src\"),\n directive(\"img-src\"),\n directive(\"font-src\"),\n directive(\"connect-src\"),\n directive(\"frame-src\"),\n // Frame ancestors: deny embedding (equivalent to X-Frame-Options: DENY)\n \"frame-ancestors 'none'\",\n // Form actions: only self\n \"form-action 'self'\",\n // Base URI: only self\n \"base-uri 'self'\",\n // Object sources: none (no plugins)\n \"object-src 'none'\",\n directive(\"worker-src\"),\n // Upgrade insecure requests in production.\n //\n // NOT under E2E_BUILD: the e2e harness runs a PRODUCTION build (for speed)\n // and serves it over plain http on a custom hostname. With this directive\n // the browser rewrites every /_next/static asset to https, which nothing is\n // listening on — every chunk fails with ERR_SSL_PROTOCOL_ERROR, the page\n // never hydrates, and each spec sees only server-rendered markup.\n env.NODE_ENV === \"production\" && env.E2E_BUILD !== \"true\" ? \"upgrade-insecure-requests\" : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n}\n","// Runtime imports: relative only. See csp.ts.\nimport { buildCsp, type CspOptions } from \"./csp\";\n\nexport interface SecurityHeaderOptions extends CspOptions {\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n}\n\nexport interface HeaderRule {\n source: string;\n headers: { key: string; value: string }[];\n}\n\nexport function buildSecurityHeaders(options: SecurityHeaderOptions, env: NodeJS.ProcessEnv): HeaderRule[] {\n const camera = options.permissionsPolicy?.camera === \"none\" ? \"\" : \"self\";\n\n return [\n {\n // Apply to all routes\n source: \"/:path*\",\n headers: [\n // Content Security Policy - prevents XSS attacks\n {\n key: \"Content-Security-Policy\",\n value: buildCsp(options, env),\n },\n // X-Frame-Options - prevents clickjacking (backup for older browsers)\n {\n key: \"X-Frame-Options\",\n value: \"DENY\",\n },\n // X-Content-Type-Options - prevents MIME sniffing\n {\n key: \"X-Content-Type-Options\",\n value: \"nosniff\",\n },\n // Strict-Transport-Security - enforces HTTPS (1 year, include subdomains).\n // Omitted under E2E_BUILD for the same reason as upgrade-insecure-requests:\n // the suite speaks plain http, and an HSTS pin would force https on every\n // later request to this host from the same browser profile.\n ...(env.E2E_BUILD === \"true\"\n ? []\n : [\n {\n key: \"Strict-Transport-Security\",\n value: \"max-age=31536000; includeSubDomains\",\n },\n ]),\n // Referrer-Policy - controls referrer information\n {\n key: \"Referrer-Policy\",\n value: \"strict-origin-when-cross-origin\",\n },\n // Permissions-Policy - restricts browser features\n {\n key: \"Permissions-Policy\",\n value: `camera=(${camera}), microphone=(), geolocation=()`,\n },\n ],\n },\n ];\n}\n","// Shared next.config for the nja fleet. Apps wrap the result with their own\n// plugins: `withNextIntl(withMDX(createNextConfig({...})))`.\n//\n// next.config.js is loaded at RUNTIME by `next start`, and the production image\n// installs with --prod. This entry may therefore import only Node built-ins at\n// runtime: never next-intl, @next/mdx, serwist or any devDependency. `next` is\n// imported for its types only.\nimport type { NextConfig } from \"next\";\nimport { buildCsp, type CspDirective, type CspService } from \"./csp\";\nimport { buildSecurityHeaders } from \"./headers\";\n\nexport { buildCsp };\nexport type { CspDirective, CspService };\n\nexport interface NjaNextConfigOptions {\n devOrigins: string[];\n /** Third-party services the app loads; each adds its own hosts to the CSP. Nothing is allowed by default. */\n services?: CspService[];\n /** Extra CSP sources per directive, for hosts no named service covers. */\n csp?: Partial<Record<CspDirective, string[]>>;\n env?: Record<string, string>;\n imageMinimumCacheTTL?: number;\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n mdx?: boolean;\n serwist?: boolean;\n securityHeaders?: boolean;\n authInterrupts?: boolean;\n reactCompiler?: boolean;\n rewrites?: NextConfig[\"rewrites\"];\n extend?: (config: NextConfig) => NextConfig;\n}\n\ntype RemotePattern = { protocol: \"http\" | \"https\"; hostname: string; port?: string };\n\nfunction parseImageSources(env: NodeJS.ProcessEnv): RemotePattern[] {\n return env.IMAGE_SOURCES\n ? env.IMAGE_SOURCES.split(\",\").map((url) => {\n const { hostname, protocol, port } = new URL(url.trim());\n return {\n protocol: protocol.replace(\":\", \"\") as RemotePattern[\"protocol\"],\n hostname,\n port: port || undefined,\n };\n })\n : [];\n}\n\nexport function createNextConfig(options: NjaNextConfigOptions): NextConfig {\n const env = process.env;\n const reactCompiler = options.reactCompiler === true;\n\n const config: NextConfig = {\n // The e2e stack (scripts/e2e.sh) runs a PRODUCTION build of this app while a\n // `next dev` server may still be running against the same worktree. `next\n // build` and `next dev` share `.next`, so a build performed underneath a live\n // dev server clobbers it and that dev server then 404s every route. Giving\n // the e2e build its own directory keeps the two independent. E2E_BUILD must\n // be set for `next build` AND for `next start`, or `start` looks in `.next`\n // and serves someone else's output.\n ...(env.E2E_BUILD === \"true\" ? { distDir: \".next-e2e\" } : {}),\n cacheComponents: true,\n partialPrefetching: true,\n reactCompiler,\n experimental: {\n serverActions: {\n bodySizeLimit: \"5mb\",\n },\n // Required by `unauthorized()` (next/navigation): without this flag it throws a plain\n // Error instead of rendering the nearest unauthorized.tsx. The flag is for app code that\n // calls it; ServerSession.checkPermission does not, it still does `redirect(\"/401\")`.\n authInterrupts: options.authInterrupts ?? true,\n turbopackGc: true,\n // No turbopackLazyDynamicImports: in `next dev` it leaves this package's lazily\n // imported request module unactivated (\"Lazily compiled dynamic import of ...\n // JsonApiRequest ... was not activated\"), so every client-side read returns\n // nothing. Seen with the package installed from npm; production builds are unaffected.\n ...(reactCompiler ? { turbopackRustReactCompiler: true } : {}),\n },\n allowedDevOrigins: options.devOrigins,\n pageExtensions: options.mdx ? [\"ts\", \"tsx\", \"mdx\"] : [\"ts\", \"tsx\"],\n images: {\n minimumCacheTTL: options.imageMinimumCacheTTL ?? 60,\n remotePatterns: parseImageSources(env),\n dangerouslyAllowLocalIP: true,\n },\n poweredByHeader: false,\n reactStrictMode: false,\n env: {\n NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL ?? \"\",\n NEXT_PUBLIC_ADDRESS: env.NEXT_PUBLIC_ADDRESS ?? \"\",\n NEXT_PUBLIC_VAPID_PUBLIC_KEY: env.NEXT_PUBLIC_VAPID_PUBLIC_KEY ?? \"\",\n NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? \"\",\n ...options.env,\n },\n // Inlined replacement for @serwist/turbopack's withSerwist wrapper, which\n // does nothing but append these two entries. Requiring the package here\n // instead CRASHES production: it is a devDependency, next.config.js is loaded\n // at RUNTIME by `next start`, and the web-production image installs with\n // --prod. Do not re-add a require of any devDependency to this file.\n ...(options.serwist ? { serverExternalPackages: [\"esbuild\", \"esbuild-wasm\"] } : {}),\n // Next 16 builds AND serves dev with Turbopack, so this is what pins yjs to a\n // single instance in both modes.\n turbopack: {\n resolveAlias: {\n yjs: \"./node_modules/yjs\",\n },\n },\n };\n\n if (options.securityHeaders !== false) {\n config.headers = async () => buildSecurityHeaders(options, process.env);\n }\n\n if (options.rewrites) config.rewrites = options.rewrites;\n\n return options.extend ? options.extend(config) : config;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/next-config/csp.ts","../../src/next-config/headers.ts","../../src/next-config/index.ts"],"sourcesContent":["// Runtime imports: none. next.config.js loads this at RUNTIME under `next start`\n// in an image installed with --prod, so it may import only Node built-ins.\n\nexport type CspDirective =\n \"script-src\" | \"style-src\" | \"img-src\" | \"font-src\" | \"connect-src\" | \"frame-src\" | \"worker-src\";\n\n/**\n * Third-party services an app may load. Each one adds its hosts to the\n * directives it needs; nothing is allowed unless the app names it.\n *\n * - `stripe`: the billing feature (`@carlonicora/nextjs-jsonapi/billing`).\n * - `googleMaps`: `FormPlaceAutocomplete` (Places API) and Maps JS/static tiles.\n * - `turnstile`: Cloudflare Turnstile widgets (contact and waitlist forms).\n * - `discord`: Discord avatars shown for users who log in with Discord.\n * - `googleFonts`: fonts linked at runtime from Google Fonts. Not needed for\n * `next/font/google`, which self-hosts at build time.\n */\nexport type CspService = \"stripe\" | \"googleMaps\" | \"turnstile\" | \"discord\" | \"googleFonts\";\n\nexport interface CspOptions {\n services?: CspService[];\n csp?: Partial<Record<CspDirective, string[]>>;\n}\n\nconst SERVICE_SOURCES: Record<CspService, Partial<Record<CspDirective, string[]>>> = {\n stripe: {\n \"script-src\": [\"https://js.stripe.com\"],\n \"img-src\": [\"https://*.stripe.com\"],\n \"connect-src\": [\"https://api.stripe.com\"],\n \"frame-src\": [\"https://js.stripe.com\", \"https://hooks.stripe.com\"],\n },\n googleMaps: {\n \"script-src\": [\"https://maps.googleapis.com\"],\n \"img-src\": [\"https://maps.googleapis.com\", \"https://maps.gstatic.com\"],\n \"connect-src\": [\"https://maps.googleapis.com\", \"https://places.googleapis.com\"],\n },\n turnstile: {\n \"script-src\": [\"https://challenges.cloudflare.com\"],\n \"connect-src\": [\"https://challenges.cloudflare.com\"],\n \"frame-src\": [\"https://challenges.cloudflare.com\"],\n },\n discord: {\n \"img-src\": [\"https://cdn.discordapp.com\"],\n },\n googleFonts: {\n \"style-src\": [\"https://fonts.googleapis.com\"],\n \"font-src\": [\"https://fonts.gstatic.com\"],\n },\n};\n\nfunction listImageSources(env: NodeJS.ProcessEnv): string[] {\n return env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((s) => s.trim()) : [];\n}\n\n/**\n * Origins of the app's own servers, the API and the corpus, from\n * `NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_CORPUS_URL`. They serve every stored\n * object link, so images and frames must be allowed from them. Unset or invalid\n * URLs are skipped; the result has no duplicates.\n */\nexport function listAppOrigins(env: NodeJS.ProcessEnv): string[] {\n const origins: string[] = [];\n for (const url of [env.NEXT_PUBLIC_API_URL, env.NEXT_PUBLIC_CORPUS_URL]) {\n if (!url) continue;\n try {\n const { origin } = new URL(url.trim());\n if (origin !== \"null\" && !origins.includes(origin)) origins.push(origin);\n } catch {\n // Not a URL: nothing to allow.\n }\n }\n return origins;\n}\n\n// Build CSP directives\n// Note: 'unsafe-inline' and 'unsafe-eval' needed for Next.js dev mode and some runtime features\n// In production, consider using nonces for stricter CSP\nexport function buildCsp(options: CspOptions, env: NodeJS.ProcessEnv): string {\n const apiUrl = env.NEXT_PUBLIC_API_URL || \"\";\n const imageSources = listImageSources(env);\n const appOrigins = listAppOrigins(env);\n const services = options.services ?? [];\n\n // Each fetch directive: what Next itself needs, then the named services, then\n // the app's extras, then the sources derived from the env.\n const fetchDirectives: Record<CspDirective, { base: string[]; fromEnv: string[] }> = {\n // Scripts: self, and unsafe-inline/eval for Next.js\n \"script-src\": { base: [\"'self'\", \"'unsafe-inline'\", \"'unsafe-eval'\"], fromEnv: [] },\n // Styles: self and unsafe-inline for styled-components/emotion\n \"style-src\": { base: [\"'self'\", \"'unsafe-inline'\"], fromEnv: [] },\n // Images: self, data URIs, blobs, configured image sources, and the api/corpus origins\n \"img-src\": { base: [\"'self'\", \"data:\", \"blob:\"], fromEnv: [...imageSources, ...appOrigins] },\n // Fonts: self\n \"font-src\": { base: [\"'self'\"], fromEnv: [] },\n // Connect: self, API (http + ws for socket.io), and storage (for uploads)\n \"connect-src\": {\n base: [\"'self'\"],\n fromEnv: [\n apiUrl,\n // Add WebSocket URL for socket.io (convert http->ws, https->wss)\n apiUrl.replace(/^http/, \"ws\"),\n env.NEXT_PUBLIC_ADDRESS || \"\",\n ...imageSources,\n ],\n },\n // Frames: self, and the api/corpus origins (inline previews of stored objects)\n \"frame-src\": { base: [\"'self'\"], fromEnv: appOrigins },\n // Worker sources: self and blob (for Web Workers with dynamic imports)\n \"worker-src\": { base: [\"'self'\", \"blob:\"], fromEnv: [] },\n };\n\n const directive = (name: CspDirective): string => {\n const { base, fromEnv } = fetchDirectives[name];\n const fromServices = services.flatMap((service) => SERVICE_SOURCES[service][name] ?? []);\n const sources = [...base, ...fromServices, ...(options.csp?.[name] ?? []), ...fromEnv].filter(\n (source) => source !== \"\",\n );\n return [name, ...new Set(sources)].join(\" \");\n };\n\n return [\n \"default-src 'self'\",\n directive(\"script-src\"),\n directive(\"style-src\"),\n directive(\"img-src\"),\n directive(\"font-src\"),\n directive(\"connect-src\"),\n directive(\"frame-src\"),\n // Frame ancestors: deny embedding (equivalent to X-Frame-Options: DENY)\n \"frame-ancestors 'none'\",\n // Form actions: only self\n \"form-action 'self'\",\n // Base URI: only self\n \"base-uri 'self'\",\n // Object sources: none (no plugins)\n \"object-src 'none'\",\n directive(\"worker-src\"),\n // Upgrade insecure requests in production.\n //\n // NOT under E2E_BUILD: the e2e harness runs a PRODUCTION build (for speed)\n // and serves it over plain http on a custom hostname. With this directive\n // the browser rewrites every /_next/static asset to https, which nothing is\n // listening on — every chunk fails with ERR_SSL_PROTOCOL_ERROR, the page\n // never hydrates, and each spec sees only server-rendered markup.\n env.NODE_ENV === \"production\" && env.E2E_BUILD !== \"true\" ? \"upgrade-insecure-requests\" : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n}\n","// Runtime imports: relative only. See csp.ts.\nimport { buildCsp, type CspOptions } from \"./csp\";\n\nexport interface SecurityHeaderOptions extends CspOptions {\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n}\n\nexport interface HeaderRule {\n source: string;\n headers: { key: string; value: string }[];\n}\n\nexport function buildSecurityHeaders(options: SecurityHeaderOptions, env: NodeJS.ProcessEnv): HeaderRule[] {\n const camera = options.permissionsPolicy?.camera === \"none\" ? \"\" : \"self\";\n\n return [\n {\n // Apply to all routes\n source: \"/:path*\",\n headers: [\n // Content Security Policy - prevents XSS attacks\n {\n key: \"Content-Security-Policy\",\n value: buildCsp(options, env),\n },\n // X-Frame-Options - prevents clickjacking (backup for older browsers)\n {\n key: \"X-Frame-Options\",\n value: \"DENY\",\n },\n // X-Content-Type-Options - prevents MIME sniffing\n {\n key: \"X-Content-Type-Options\",\n value: \"nosniff\",\n },\n // Strict-Transport-Security - enforces HTTPS (1 year, include subdomains).\n // Omitted under E2E_BUILD for the same reason as upgrade-insecure-requests:\n // the suite speaks plain http, and an HSTS pin would force https on every\n // later request to this host from the same browser profile.\n ...(env.E2E_BUILD === \"true\"\n ? []\n : [\n {\n key: \"Strict-Transport-Security\",\n value: \"max-age=31536000; includeSubDomains\",\n },\n ]),\n // Referrer-Policy - controls referrer information\n {\n key: \"Referrer-Policy\",\n value: \"strict-origin-when-cross-origin\",\n },\n // Permissions-Policy - restricts browser features\n {\n key: \"Permissions-Policy\",\n value: `camera=(${camera}), microphone=(), geolocation=()`,\n },\n ],\n },\n ];\n}\n","// Shared next.config for the nja fleet. Apps wrap the result with their own\n// plugins: `withNextIntl(withMDX(createNextConfig({...})))`.\n//\n// next.config.js is loaded at RUNTIME by `next start`, and the production image\n// installs with --prod. This entry may therefore import only Node built-ins at\n// runtime: never next-intl, @next/mdx, serwist or any devDependency. `next` is\n// imported for its types only.\nimport type { NextConfig } from \"next\";\nimport { buildCsp, listAppOrigins, type CspDirective, type CspService } from \"./csp\";\nimport { buildSecurityHeaders } from \"./headers\";\n\nexport { buildCsp, listAppOrigins };\nexport type { CspDirective, CspService };\n\nexport interface NjaNextConfigOptions {\n devOrigins: string[];\n /** Third-party services the app loads; each adds its own hosts to the CSP. Nothing is allowed by default. */\n services?: CspService[];\n /** Extra CSP sources per directive, for hosts no named service covers. */\n csp?: Partial<Record<CspDirective, string[]>>;\n env?: Record<string, string>;\n imageMinimumCacheTTL?: number;\n permissionsPolicy?: { camera?: \"none\" | \"self\" };\n mdx?: boolean;\n serwist?: boolean;\n securityHeaders?: boolean;\n authInterrupts?: boolean;\n reactCompiler?: boolean;\n rewrites?: NextConfig[\"rewrites\"];\n extend?: (config: NextConfig) => NextConfig;\n}\n\ntype RemotePattern = { protocol: \"http\" | \"https\"; hostname: string; port?: string };\n\n// IMAGE_SOURCES, then the api and corpus origins (they serve every stored object\n// link), without duplicates.\nfunction parseImageSources(env: NodeJS.ProcessEnv): RemotePattern[] {\n const imageSources = env.IMAGE_SOURCES ? env.IMAGE_SOURCES.split(\",\").map((url) => url.trim()) : [];\n const patterns: RemotePattern[] = [];\n for (const url of [...imageSources, ...listAppOrigins(env)]) {\n const { hostname, protocol, port } = new URL(url);\n const pattern: RemotePattern = {\n protocol: protocol.replace(\":\", \"\") as RemotePattern[\"protocol\"],\n hostname,\n port: port || undefined,\n };\n const seen = patterns.some(\n (p) => p.protocol === pattern.protocol && p.hostname === pattern.hostname && p.port === pattern.port,\n );\n if (!seen) patterns.push(pattern);\n }\n return patterns;\n}\n\nexport function createNextConfig(options: NjaNextConfigOptions): NextConfig {\n const env = process.env;\n const reactCompiler = options.reactCompiler === true;\n\n const config: NextConfig = {\n // The e2e stack (scripts/e2e.sh) runs a PRODUCTION build of this app while a\n // `next dev` server may still be running against the same worktree. `next\n // build` and `next dev` share `.next`, so a build performed underneath a live\n // dev server clobbers it and that dev server then 404s every route. Giving\n // the e2e build its own directory keeps the two independent. E2E_BUILD must\n // be set for `next build` AND for `next start`, or `start` looks in `.next`\n // and serves someone else's output.\n ...(env.E2E_BUILD === \"true\" ? { distDir: \".next-e2e\" } : {}),\n cacheComponents: true,\n partialPrefetching: true,\n reactCompiler,\n experimental: {\n serverActions: {\n bodySizeLimit: \"5mb\",\n },\n // Required by `unauthorized()` (next/navigation): without this flag it throws a plain\n // Error instead of rendering the nearest unauthorized.tsx. The flag is for app code that\n // calls it; ServerSession.checkPermission does not, it still does `redirect(\"/401\")`.\n authInterrupts: options.authInterrupts ?? true,\n turbopackGc: true,\n // No turbopackLazyDynamicImports: in `next dev` it leaves this package's lazily\n // imported request module unactivated (\"Lazily compiled dynamic import of ...\n // JsonApiRequest ... was not activated\"), so every client-side read returns\n // nothing. Seen with the package installed from npm; production builds are unaffected.\n ...(reactCompiler ? { turbopackRustReactCompiler: true } : {}),\n },\n allowedDevOrigins: options.devOrigins,\n pageExtensions: options.mdx ? [\"ts\", \"tsx\", \"mdx\"] : [\"ts\", \"tsx\"],\n images: {\n minimumCacheTTL: options.imageMinimumCacheTTL ?? 60,\n remotePatterns: parseImageSources(env),\n dangerouslyAllowLocalIP: true,\n },\n poweredByHeader: false,\n reactStrictMode: false,\n env: {\n NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL ?? \"\",\n NEXT_PUBLIC_ADDRESS: env.NEXT_PUBLIC_ADDRESS ?? \"\",\n NEXT_PUBLIC_VAPID_PUBLIC_KEY: env.NEXT_PUBLIC_VAPID_PUBLIC_KEY ?? \"\",\n NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? \"\",\n ...options.env,\n },\n // Inlined replacement for @serwist/turbopack's withSerwist wrapper, which\n // does nothing but append these two entries. Requiring the package here\n // instead CRASHES production: it is a devDependency, next.config.js is loaded\n // at RUNTIME by `next start`, and the web-production image installs with\n // --prod. Do not re-add a require of any devDependency to this file.\n ...(options.serwist ? { serverExternalPackages: [\"esbuild\", \"esbuild-wasm\"] } : {}),\n // Next 16 builds AND serves dev with Turbopack, so this is what pins yjs to a\n // single instance in both modes.\n turbopack: {\n resolveAlias: {\n yjs: \"./node_modules/yjs\",\n },\n },\n };\n\n if (options.securityHeaders !== false) {\n config.headers = async () => buildSecurityHeaders(options, process.env);\n }\n\n if (options.rewrites) config.rewrites = options.rewrites;\n\n return options.extend ? options.extend(config) : config;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import { ReactNode } from "react";
|
|
|
7
7
|
import { AttributeElement } from "../../../../components";
|
|
8
8
|
import { SectionHeader, MicroLabel } from "../../../../components/typography";
|
|
9
9
|
import { FiscalDataDisplay } from "../../../../components/fiscal/FiscalDataDisplay";
|
|
10
|
-
import { cn } from "../../../../utils";
|
|
10
|
+
import { cn, isRemoteImageSrc } from "../../../../utils";
|
|
11
11
|
import { CompanyInterface } from "../../data";
|
|
12
12
|
|
|
13
13
|
type CompanyContentProps = {
|
|
@@ -44,7 +44,14 @@ export function CompanyContent({ company, actions }: CompanyContentProps) {
|
|
|
44
44
|
{/* Hero Section */}
|
|
45
45
|
<div className="flex items-start gap-x-6">
|
|
46
46
|
{company.logo && (
|
|
47
|
-
<Image
|
|
47
|
+
<Image
|
|
48
|
+
src={company.logo}
|
|
49
|
+
alt={company.name}
|
|
50
|
+
width={150}
|
|
51
|
+
height={150}
|
|
52
|
+
className="rounded-md"
|
|
53
|
+
unoptimized={isRemoteImageSrc(company.logo)}
|
|
54
|
+
/>
|
|
48
55
|
)}
|
|
49
56
|
<div className="flex flex-col gap-y-2">
|
|
50
57
|
{company.legal_address && (
|
|
@@ -26,6 +26,7 @@ import { Modules } from "../../../../core";
|
|
|
26
26
|
import { useI18nRouter } from "../../../../i18n";
|
|
27
27
|
import { getRoleId } from "../../../../roles";
|
|
28
28
|
import { ScrollArea } from "../../../../shadcnui";
|
|
29
|
+
import { isRemoteImageSrc } from "../../../../utils";
|
|
29
30
|
import { FeatureInterface } from "../../../feature";
|
|
30
31
|
import { FeatureService } from "../../../feature/data/feature.service";
|
|
31
32
|
import { S3Interface } from "../../../s3";
|
|
@@ -232,6 +233,7 @@ function CompanyEditorInternal({
|
|
|
232
233
|
alt="Company Logo"
|
|
233
234
|
width={200}
|
|
234
235
|
height={200}
|
|
236
|
+
unoptimized={isRemoteImageSrc(file ? undefined : company?.logo)}
|
|
235
237
|
/>
|
|
236
238
|
) : (
|
|
237
239
|
<>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, afterAll, beforeAll, vi } from "vitest";
|
|
2
2
|
import { renderHook } from "@testing-library/react";
|
|
3
3
|
|
|
4
4
|
// Mutable mocks the individual tests below configure per case, mirroring the
|
|
@@ -22,13 +22,37 @@ vi.mock("../../../client/config", () => ({
|
|
|
22
22
|
|
|
23
23
|
import { useSubscriptionStatus } from "./useSubscriptionStatus";
|
|
24
24
|
|
|
25
|
+
const DAY_MS = 1000 * 60 * 60 * 24;
|
|
26
|
+
|
|
27
|
+
// Every date case runs on a frozen clock in a zone with DST. The window math
|
|
28
|
+
// used to follow the host's local calendar, so the result depended on the day
|
|
29
|
+
// the suite ran and on the machine's timezone; pinning both makes it repeatable.
|
|
30
|
+
const ORIGINAL_TZ = process.env.TZ;
|
|
31
|
+
const FIXED_NOW = new Date("2026-06-15T12:00:00+02:00"); // no DST change within 14 days
|
|
32
|
+
|
|
33
|
+
const daysAgo = (days: number, now: Date = FIXED_NOW) => new Date(now.getTime() - days * DAY_MS);
|
|
34
|
+
|
|
25
35
|
describe("useSubscriptionStatus", () => {
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
process.env.TZ = "Europe/Rome";
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
process.env.TZ = ORIGINAL_TZ;
|
|
42
|
+
});
|
|
43
|
+
|
|
26
44
|
beforeEach(() => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
vi.setSystemTime(FIXED_NOW);
|
|
27
47
|
userMock.currentUser = null;
|
|
28
48
|
userMock.company = null;
|
|
29
49
|
configMock.stripeKey = "pk_test_123";
|
|
30
50
|
});
|
|
31
51
|
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.useRealTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
32
56
|
it("reports loading while currentUser is still null", () => {
|
|
33
57
|
userMock.currentUser = null;
|
|
34
58
|
|
|
@@ -69,7 +93,7 @@ describe("useSubscriptionStatus", () => {
|
|
|
69
93
|
|
|
70
94
|
it("is never blocked when the company has an active subscription", () => {
|
|
71
95
|
userMock.currentUser = { roles: [] };
|
|
72
|
-
userMock.company = { isActiveSubscription: true, createdAt:
|
|
96
|
+
userMock.company = { isActiveSubscription: true, createdAt: FIXED_NOW };
|
|
73
97
|
|
|
74
98
|
const { result } = renderHook(() => useSubscriptionStatus());
|
|
75
99
|
|
|
@@ -79,8 +103,7 @@ describe("useSubscriptionStatus", () => {
|
|
|
79
103
|
|
|
80
104
|
it("does NOT block an unsubscribed company that is still inside its trial window", () => {
|
|
81
105
|
userMock.currentUser = { roles: [] };
|
|
82
|
-
const createdAt =
|
|
83
|
-
createdAt.setDate(createdAt.getDate() - 1); // 1 day old — squarely inside the 14-day trial
|
|
106
|
+
const createdAt = daysAgo(1); // 1 day old — squarely inside the 14-day trial
|
|
84
107
|
userMock.company = { isActiveSubscription: false, createdAt };
|
|
85
108
|
|
|
86
109
|
const { result } = renderHook(() => useSubscriptionStatus());
|
|
@@ -94,8 +117,7 @@ describe("useSubscriptionStatus", () => {
|
|
|
94
117
|
|
|
95
118
|
it("blocks once the trial window has elapsed with no active subscription", () => {
|
|
96
119
|
userMock.currentUser = { roles: [] };
|
|
97
|
-
const createdAt =
|
|
98
|
-
createdAt.setDate(createdAt.getDate() - 15); // past the 14-day trial
|
|
120
|
+
const createdAt = daysAgo(15); // past the 14-day trial
|
|
99
121
|
userMock.company = { isActiveSubscription: false, createdAt };
|
|
100
122
|
|
|
101
123
|
const { result } = renderHook(() => useSubscriptionStatus());
|
|
@@ -106,8 +128,7 @@ describe("useSubscriptionStatus", () => {
|
|
|
106
128
|
|
|
107
129
|
it("keeps daysRemaining counting down from createdAt, unaffected by the isBlocked change (1 day old)", () => {
|
|
108
130
|
userMock.currentUser = { roles: [] };
|
|
109
|
-
const createdAt =
|
|
110
|
-
createdAt.setDate(createdAt.getDate() - 1);
|
|
131
|
+
const createdAt = daysAgo(1);
|
|
111
132
|
userMock.company = { isActiveSubscription: false, createdAt };
|
|
112
133
|
|
|
113
134
|
const { result } = renderHook(() => useSubscriptionStatus());
|
|
@@ -118,8 +139,7 @@ describe("useSubscriptionStatus", () => {
|
|
|
118
139
|
|
|
119
140
|
it("keeps isGracePeriod true once daysRemaining is inside GRACE_DAYS (12 days old)", () => {
|
|
120
141
|
userMock.currentUser = { roles: [] };
|
|
121
|
-
const createdAt =
|
|
122
|
-
createdAt.setDate(createdAt.getDate() - 12);
|
|
142
|
+
const createdAt = daysAgo(12);
|
|
123
143
|
userMock.company = { isActiveSubscription: false, createdAt };
|
|
124
144
|
|
|
125
145
|
const { result } = renderHook(() => useSubscriptionStatus());
|
|
@@ -127,4 +147,50 @@ describe("useSubscriptionStatus", () => {
|
|
|
127
147
|
expect(result.current.daysRemaining).toBe(2);
|
|
128
148
|
expect(result.current.isGracePeriod).toBe(true);
|
|
129
149
|
});
|
|
150
|
+
|
|
151
|
+
// Regression: on 2026-10-12 in Europe/Rome the 1-day-old case reported 14.
|
|
152
|
+
// The trial end (2026-10-25) falls on the day CEST ends, so adding 14 local
|
|
153
|
+
// calendar days produced 14 days + 1 hour, and ceil(13.04) = 14.
|
|
154
|
+
it("reports 13 days for a 1-day-old company when the trial end crosses the autumn DST change", () => {
|
|
155
|
+
const now = new Date("2026-10-12T14:18:02+02:00");
|
|
156
|
+
vi.setSystemTime(now);
|
|
157
|
+
userMock.currentUser = { roles: [] };
|
|
158
|
+
const createdAt = daysAgo(1, now);
|
|
159
|
+
userMock.company = { isActiveSubscription: false, createdAt };
|
|
160
|
+
|
|
161
|
+
const { result } = renderHook(() => useSubscriptionStatus());
|
|
162
|
+
|
|
163
|
+
expect(result.current.daysRemaining).toBe(13);
|
|
164
|
+
expect(result.current.trialEndsAt?.getTime()).toBe(createdAt.getTime() + 14 * DAY_MS);
|
|
165
|
+
expect(result.current.status).toBe("trial");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("reports 13 days for a 1-day-old company when the trial end crosses the spring DST change", () => {
|
|
169
|
+
const now = new Date("2027-03-20T12:00:00+01:00"); // trial ends after CET -> CEST on 2027-03-28
|
|
170
|
+
vi.setSystemTime(now);
|
|
171
|
+
userMock.currentUser = { roles: [] };
|
|
172
|
+
const createdAt = daysAgo(1, now);
|
|
173
|
+
userMock.company = { isActiveSubscription: false, createdAt };
|
|
174
|
+
|
|
175
|
+
const { result } = renderHook(() => useSubscriptionStatus());
|
|
176
|
+
|
|
177
|
+
expect(result.current.daysRemaining).toBe(13);
|
|
178
|
+
expect(result.current.trialEndsAt?.getTime()).toBe(createdAt.getTime() + 14 * DAY_MS);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("blocks exactly 14 x 24h after createdAt, even when the window spans a DST change", () => {
|
|
182
|
+
const createdAt = new Date("2026-10-12T14:18:02+02:00");
|
|
183
|
+
userMock.currentUser = { roles: [] };
|
|
184
|
+
userMock.company = { isActiveSubscription: false, createdAt };
|
|
185
|
+
|
|
186
|
+
vi.setSystemTime(createdAt.getTime() + 14 * DAY_MS - 1);
|
|
187
|
+
const before = renderHook(() => useSubscriptionStatus());
|
|
188
|
+
expect(before.result.current.isBlocked).toBe(false);
|
|
189
|
+
expect(before.result.current.daysRemaining).toBe(1);
|
|
190
|
+
|
|
191
|
+
vi.setSystemTime(createdAt.getTime() + 14 * DAY_MS);
|
|
192
|
+
const after = renderHook(() => useSubscriptionStatus());
|
|
193
|
+
expect(after.result.current.isBlocked).toBe(true);
|
|
194
|
+
expect(after.result.current.status).toBe("expired");
|
|
195
|
+
});
|
|
130
196
|
});
|
|
@@ -15,6 +15,7 @@ export interface TrialSubscriptionStatus {
|
|
|
15
15
|
|
|
16
16
|
const TRIAL_DAYS = 14;
|
|
17
17
|
const GRACE_DAYS = 3;
|
|
18
|
+
const DAY_MS = 1000 * 60 * 60 * 24;
|
|
18
19
|
|
|
19
20
|
const isAdministrator = (currentUser: any): boolean => {
|
|
20
21
|
if (!currentUser || !isRolesConfigured()) return false;
|
|
@@ -93,18 +94,17 @@ export function useSubscriptionStatus(): TrialSubscriptionStatus {
|
|
|
93
94
|
// trialing/active, and that is not guaranteed — a real signup produced a
|
|
94
95
|
// StripeSubscription stuck at status "incomplete", leaving the flag NULL and
|
|
95
96
|
// a zero-day-old company blocked out of its own trial.
|
|
97
|
+
//
|
|
98
|
+
// The trial is a fixed duration (Stripe's trial_period_days is 14 x 24h),
|
|
99
|
+
// so add it in milliseconds. Adding calendar days with setDate() follows the
|
|
100
|
+
// viewer's local clock: across a DST change the window grows or shrinks by
|
|
101
|
+
// an hour, and dividing that by a fixed 24h day then shows one day too many.
|
|
96
102
|
const createdAt = new Date(company.createdAt);
|
|
97
|
-
const trialEndsAt = new Date(createdAt);
|
|
98
|
-
trialEndsAt.setDate(trialEndsAt.getDate() + TRIAL_DAYS);
|
|
103
|
+
const trialEndsAt = new Date(createdAt.getTime() + TRIAL_DAYS * DAY_MS);
|
|
99
104
|
|
|
100
105
|
const now = new Date();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// day count plus or minus an hour, and ceil turned 13 days + 1h into 14.
|
|
104
|
-
// Correcting by the offset difference removes exactly that hour.
|
|
105
|
-
const dstShiftMs = (now.getTimezoneOffset() - trialEndsAt.getTimezoneOffset()) * 60 * 1000;
|
|
106
|
-
const msRemaining = trialEndsAt.getTime() - now.getTime() + dstShiftMs;
|
|
107
|
-
const daysRemaining = Math.ceil(msRemaining / (1000 * 60 * 60 * 24));
|
|
106
|
+
const msRemaining = trialEndsAt.getTime() - now.getTime();
|
|
107
|
+
const daysRemaining = Math.ceil(msRemaining / DAY_MS);
|
|
108
108
|
const trialElapsed = daysRemaining <= 0;
|
|
109
109
|
|
|
110
110
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
2
2
|
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { ReactElement } from "react";
|
|
4
|
-
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ReactElement, useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
7
7
|
import { SidebarProvider } from "../../../../shadcnui";
|
|
@@ -25,9 +25,10 @@ const guides = [
|
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
const { useDataListRetriever } = vi.hoisted(() =>
|
|
29
|
-
|
|
30
|
-
}
|
|
28
|
+
const { useDataListRetriever, defaultRetriever } = vi.hoisted(() => {
|
|
29
|
+
const defaultRetriever = () => ({ data: guides, search: () => {}, refresh: () => {} });
|
|
30
|
+
return { useDataListRetriever: vi.fn(defaultRetriever), defaultRetriever };
|
|
31
|
+
});
|
|
31
32
|
|
|
32
33
|
vi.mock("../../../../hooks", async (importOriginal) => ({
|
|
33
34
|
...(await importOriginal<typeof import("../../../../hooks")>()),
|
|
@@ -36,6 +37,14 @@ vi.mock("../../../../hooks", async (importOriginal) => ({
|
|
|
36
37
|
}));
|
|
37
38
|
vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
|
|
38
39
|
|
|
40
|
+
const { viewerSpy } = vi.hoisted(() => ({ viewerSpy: vi.fn() }));
|
|
41
|
+
vi.mock("./HowToCommandViewer", () => ({
|
|
42
|
+
default: ({ howTo }: { howTo: { id: string } }) => {
|
|
43
|
+
viewerSpy(howTo);
|
|
44
|
+
return <div data-testid="howto-viewer" />;
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
|
|
39
48
|
beforeAll(() => {
|
|
40
49
|
ModuleRegistry.register("HowTo" as any, { name: "howtos", model: HowTo } as any);
|
|
41
50
|
});
|
|
@@ -70,3 +79,67 @@ describe("HowToCommand relevance", () => {
|
|
|
70
79
|
expect(relevant).not.toHaveTextContent("Guida contatti");
|
|
71
80
|
});
|
|
72
81
|
});
|
|
82
|
+
|
|
83
|
+
// Two guides with the SAME title: cmdk derives an item's value from its text when no
|
|
84
|
+
// `value` is given, so without a unique value both rows highlight together.
|
|
85
|
+
describe("HowToCommand same-titled guides", () => {
|
|
86
|
+
const sameTitled = [
|
|
87
|
+
{ id: "s1", name: "Come creare una pratica", pages: "[]", contextualKeys: [], draft: false },
|
|
88
|
+
{ id: "s2", name: "Come creare una pratica", pages: "[]", contextualKeys: [], draft: false },
|
|
89
|
+
{ id: "s3", name: "Gestire i contatti", pages: "[]", contextualKeys: [], draft: false },
|
|
90
|
+
];
|
|
91
|
+
const searchSpy = vi.fn();
|
|
92
|
+
|
|
93
|
+
beforeAll(() => {
|
|
94
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
useDataListRetriever.mockImplementation(defaultRetriever);
|
|
99
|
+
searchSpy.mockReset();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function useFakeRetriever() {
|
|
103
|
+
const [data, setData] = useState<any[]>(sameTitled);
|
|
104
|
+
const search = useCallback(async (term: string) => {
|
|
105
|
+
searchSpy(term);
|
|
106
|
+
setData(sameTitled.filter((g) => g.name.toLowerCase().includes(term.toLowerCase())));
|
|
107
|
+
}, []);
|
|
108
|
+
return useMemo(() => ({ data, search, refresh: () => {} }), [data, search]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it("highlights only one of two same-titled guides and opens the one clicked", async () => {
|
|
112
|
+
useDataListRetriever.mockImplementation(useFakeRetriever as any);
|
|
113
|
+
renderCommand(<HowToCommand pathname="/anything" />);
|
|
114
|
+
await open();
|
|
115
|
+
const list = await screen.findByRole("listbox");
|
|
116
|
+
await waitFor(() => expect(within(list).getAllByRole("option")).toHaveLength(3));
|
|
117
|
+
|
|
118
|
+
const highlighted = within(list)
|
|
119
|
+
.getAllByRole("option")
|
|
120
|
+
.filter((o) => o.getAttribute("aria-selected") === "true");
|
|
121
|
+
expect(highlighted).toHaveLength(1);
|
|
122
|
+
|
|
123
|
+
const rows = within(list).getAllByRole("option", { name: "Come creare una pratica" });
|
|
124
|
+
expect(rows).toHaveLength(2);
|
|
125
|
+
await userEvent.click(rows[1]);
|
|
126
|
+
// The viewer opens on the clicked guide: the dialog title becomes its name, and
|
|
127
|
+
// HowToCommandViewer receives guide s2.
|
|
128
|
+
expect(await screen.findByRole("heading", { name: "Come creare una pratica" })).toBeInTheDocument();
|
|
129
|
+
expect(viewerSpy).toHaveBeenLastCalledWith(expect.objectContaining({ id: "s2" }));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("still filters the list when the user types", async () => {
|
|
133
|
+
useDataListRetriever.mockImplementation(useFakeRetriever as any);
|
|
134
|
+
renderCommand(<HowToCommand pathname="/anything" />);
|
|
135
|
+
await open();
|
|
136
|
+
const list = await screen.findByRole("listbox");
|
|
137
|
+
await waitFor(() => expect(within(list).getAllByRole("option")).toHaveLength(3));
|
|
138
|
+
|
|
139
|
+
await userEvent.type(screen.getByRole("combobox"), "contatti");
|
|
140
|
+
|
|
141
|
+
await waitFor(() => expect(searchSpy).toHaveBeenCalledWith("contatti"));
|
|
142
|
+
await waitFor(() => expect(within(list).getAllByRole("option")).toHaveLength(1));
|
|
143
|
+
expect(within(list).getByRole("option", { name: "Gestire i contatti" })).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
});
|