@rvoh/psychic 3.1.3 → 3.2.1

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 (86) hide show
  1. package/dist/cjs/src/bin/helpers/OpenApiSpecDiff.js +14 -2
  2. package/dist/cjs/src/bin/index.js +14 -1
  3. package/dist/cjs/src/cli/helpers/PackageManager.js +18 -14
  4. package/dist/cjs/src/cli/index.js +2 -0
  5. package/dist/cjs/src/controller/index.js +105 -19
  6. package/dist/cjs/src/devtools/helpers/launchDevServer.js +4 -0
  7. package/dist/cjs/src/encrypt/internal-encrypt.js +2 -16
  8. package/dist/cjs/src/error/devtools/LaunchDevServerRequiresDevelopmentOrTest.js +22 -0
  9. package/dist/cjs/src/error/http/InternalServerError.js +4 -0
  10. package/dist/cjs/src/error/openapi/OpenApiSpecDiffRequiresDevelopmentOrTest.js +15 -0
  11. package/dist/cjs/src/generate/controller.js +3 -1
  12. package/dist/cjs/src/generate/helpers/generateControllerContent.js +33 -3
  13. package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +1 -23
  14. package/dist/cjs/src/generate/helpers/paramSafeColumnNamesFromCliTokens.js +35 -0
  15. package/dist/cjs/src/generate/helpers/parseAttribute.js +35 -0
  16. package/dist/cjs/src/generate/helpers/reduxBindings/writeInitializer.js +3 -2
  17. package/dist/cjs/src/generate/helpers/syncOpenapiTypescript/generateInitializer.js +2 -2
  18. package/dist/cjs/src/generate/helpers/syncOpenapiTypescript/installOpenapiTypescript.js +3 -3
  19. package/dist/cjs/src/generate/helpers/zustandBindings/writeInitializer.js +3 -2
  20. package/dist/cjs/src/generate/openapi/reduxBindings.js +2 -1
  21. package/dist/cjs/src/generate/openapi/zustandBindings.js +2 -1
  22. package/dist/cjs/src/generate/resource.js +9 -0
  23. package/dist/cjs/src/helpers/isSafeRedirectTarget.js +64 -0
  24. package/dist/cjs/src/helpers/validateOpenApiSchema.js +2 -2
  25. package/dist/cjs/src/i18n/provider.js +9 -0
  26. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +13 -3
  27. package/dist/cjs/src/psychic-app/index.js +52 -10
  28. package/dist/cjs/src/router/helpers.js +4 -1
  29. package/dist/cjs/src/router/index.js +2 -1
  30. package/dist/cjs/src/server/index.js +17 -2
  31. package/dist/cjs/src/server/params.js +38 -0
  32. package/dist/cjs/src/session/index.js +6 -0
  33. package/dist/cjs/src/watcher/Watcher.js +2 -1
  34. package/dist/esm/src/bin/helpers/OpenApiSpecDiff.js +14 -2
  35. package/dist/esm/src/bin/index.js +14 -1
  36. package/dist/esm/src/cli/helpers/PackageManager.js +18 -14
  37. package/dist/esm/src/cli/index.js +2 -0
  38. package/dist/esm/src/controller/index.js +105 -19
  39. package/dist/esm/src/devtools/helpers/launchDevServer.js +4 -0
  40. package/dist/esm/src/encrypt/internal-encrypt.js +2 -16
  41. package/dist/esm/src/error/devtools/LaunchDevServerRequiresDevelopmentOrTest.js +22 -0
  42. package/dist/esm/src/error/http/InternalServerError.js +4 -0
  43. package/dist/esm/src/error/openapi/OpenApiSpecDiffRequiresDevelopmentOrTest.js +15 -0
  44. package/dist/esm/src/generate/controller.js +3 -1
  45. package/dist/esm/src/generate/helpers/generateControllerContent.js +33 -3
  46. package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +1 -23
  47. package/dist/esm/src/generate/helpers/paramSafeColumnNamesFromCliTokens.js +35 -0
  48. package/dist/esm/src/generate/helpers/parseAttribute.js +35 -0
  49. package/dist/esm/src/generate/helpers/reduxBindings/writeInitializer.js +3 -2
  50. package/dist/esm/src/generate/helpers/syncOpenapiTypescript/generateInitializer.js +2 -2
  51. package/dist/esm/src/generate/helpers/syncOpenapiTypescript/installOpenapiTypescript.js +3 -3
  52. package/dist/esm/src/generate/helpers/zustandBindings/writeInitializer.js +3 -2
  53. package/dist/esm/src/generate/openapi/reduxBindings.js +2 -1
  54. package/dist/esm/src/generate/openapi/zustandBindings.js +2 -1
  55. package/dist/esm/src/generate/resource.js +9 -0
  56. package/dist/esm/src/helpers/isSafeRedirectTarget.js +64 -0
  57. package/dist/esm/src/helpers/validateOpenApiSchema.js +2 -2
  58. package/dist/esm/src/i18n/provider.js +9 -0
  59. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +13 -3
  60. package/dist/esm/src/psychic-app/index.js +52 -10
  61. package/dist/esm/src/router/helpers.js +4 -1
  62. package/dist/esm/src/router/index.js +2 -1
  63. package/dist/esm/src/server/index.js +17 -2
  64. package/dist/esm/src/server/params.js +38 -0
  65. package/dist/esm/src/session/index.js +6 -0
  66. package/dist/esm/src/watcher/Watcher.js +2 -1
  67. package/dist/types/src/bin/helpers/OpenApiSpecDiff.d.ts +2 -1
  68. package/dist/types/src/bin/index.d.ts +2 -0
  69. package/dist/types/src/cli/helpers/PackageManager.d.ts +7 -3
  70. package/dist/types/src/cli/index.d.ts +2 -0
  71. package/dist/types/src/controller/index.d.ts +88 -15
  72. package/dist/types/src/encrypt/internal-encrypt.d.ts +1 -3
  73. package/dist/types/src/error/devtools/LaunchDevServerRequiresDevelopmentOrTest.d.ts +5 -0
  74. package/dist/types/src/error/http/InternalServerError.d.ts +4 -0
  75. package/dist/types/src/error/openapi/OpenApiSpecDiffRequiresDevelopmentOrTest.d.ts +3 -0
  76. package/dist/types/src/generate/controller.d.ts +3 -1
  77. package/dist/types/src/generate/helpers/generateControllerContent.d.ts +4 -1
  78. package/dist/types/src/generate/helpers/paramSafeColumnNamesFromCliTokens.d.ts +20 -0
  79. package/dist/types/src/generate/helpers/parseAttribute.d.ts +18 -0
  80. package/dist/types/src/generate/resource.d.ts +2 -0
  81. package/dist/types/src/helpers/isSafeRedirectTarget.d.ts +25 -0
  82. package/dist/types/src/helpers/validateOpenApiSchema.d.ts +4 -3
  83. package/dist/types/src/i18n/provider.d.ts +9 -0
  84. package/dist/types/src/psychic-app/index.d.ts +40 -7
  85. package/dist/types/src/server/params.d.ts +46 -0
  86. package/package.json +5 -4
@@ -9,6 +9,15 @@ export default class I18nProvider {
9
9
  *
10
10
  * @param allLocales - the list of all locales in your app. something like `{ en: { ... }, ['en-UK']: { ... }, es: { ... }, ... }`
11
11
  * @param singleLocaleKey - the key from the allLocales object that you want to use as your type base. i.e. 'en'
12
+ *
13
+ * Output-encoding note: interpolated values are substituted verbatim into the
14
+ * translation string. There is no HTML escaping at this layer — Psychic emits
15
+ * JSON, and the client renderer (React / Vue / Svelte / etc.) is responsible
16
+ * for context-appropriate escaping when the translated string enters a DOM.
17
+ * Do not pipe translated strings through `dangerouslySetInnerHTML` / `v-html` /
18
+ * `{@html}`. For rich-text translations, use structured dictionaries instead
19
+ * of HTML inside translation values. See `psychic-guides` → Security →
20
+ * Output Encoding & i18n.
12
21
  * */
13
22
  static provide<const AllLocales, const SingleLocaleKey extends keyof AllLocales & string, LocalesEnum extends string = (typeof I18nDefaultLocales)[number], SingleLocaleShape = AllLocales[SingleLocaleKey]>(allLocales: AllLocales, singleLocaleKey: SingleLocaleKey): (locale: LocalesEnum, i18nPathString: DottedLanguageObjectStringPaths<SingleLocaleShape> & string, interpolations?: Record<string, string | number>) => string;
14
23
  }
@@ -101,13 +101,23 @@ export default class PsychicApp {
101
101
  /**
102
102
  * @internal
103
103
  *
104
- * adds the necessary package manager prefix to the psy command provided
105
- * i.e. `psyCmd('sync')`
104
+ * Returns the argv-form invocation for a `psy` subcommand, ready to pass
105
+ * to `DreamCLI.spawn(command, { args })`. Example: `psyCmd('sync')`
106
+ * `{ command: 'pnpm', args: ['psy', 'sync'] }` (under pnpm).
106
107
  */
107
- psyCmd(cmd: string): string;
108
+ psyCmd(cmd: string, args?: string[]): import("../cli/helpers/PackageManager.js").PackageManagerCommand;
108
109
  /**
109
- * prints a warning in the console if the encryption key
110
- * is not valid for the provided algorithm.
110
+ * Throws (in production) or prints a warning (in non-production) if the
111
+ * encryption key is not valid for the provided algorithm.
112
+ *
113
+ * In production, a misconfigured key is a security hazard — the app would
114
+ * appear to boot successfully, then fail at runtime the first time a cookie
115
+ * is encrypted or decrypted, potentially leaving users with mysterious
116
+ * session errors long after the deploy. Fail-closed at boot instead.
117
+ *
118
+ * In development, continue to warn so that onboarding flows (where a
119
+ * placeholder key may briefly be in place while the env is being set up)
120
+ * are not broken by a hard failure.
111
121
  *
112
122
  * @param encryptionIdentifier - currently must be 'cookies', though this may change in the future
113
123
  * @param key - the encryption key you want to check
@@ -144,7 +154,30 @@ export default class PsychicApp {
144
154
  get httpServerOptions(): http.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse> | https.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse>;
145
155
  private _corsOptions;
146
156
  get corsOptions(): cors.Options;
157
+ private _redirectAllowedHosts;
158
+ /**
159
+ * Hostnames allowlisted for absolute-URL redirects issued via
160
+ * controller `redirect()` / `found()` / `movedPermanently()` / etc.
161
+ * Same-origin relative paths are always permitted; absolute URLs are
162
+ * rejected unless their hostname appears here (case-insensitive). The
163
+ * default is an empty allowlist, meaning external redirects fail-closed.
164
+ */
165
+ get redirectAllowedHosts(): readonly string[];
147
166
  private _jsonOptions;
167
+ /**
168
+ * Options passed through to `koa-bodyparser`. When unset, the upstream
169
+ * defaults apply — notably `jsonLimit: '1mb'` and `formLimit: '56kb'`,
170
+ * which cap request-body size at the framework boundary and defend against
171
+ * unbounded-payload memory DoS even when no edge (WAF / API Gateway)
172
+ * enforces a cap.
173
+ *
174
+ * Configure via `psy.set('json', { jsonLimit: '256kb' })` to tighten.
175
+ * Values are spread-merged, so partial overrides preserve untouched limits.
176
+ *
177
+ * See https://github.com/koajs/bodyparser for the full option list
178
+ * (`jsonLimit`, `formLimit`, `textLimit`, `xmlLimit`, `enableTypes`,
179
+ * `strict`, `detectJSON`, etc.).
180
+ */
148
181
  get jsonOptions(): bodyParser.Options;
149
182
  private _cookieOptions;
150
183
  get cookieOptions(): {
@@ -222,10 +255,10 @@ export default class PsychicApp {
222
255
  plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
223
256
  on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, ctx: Koa.Context) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
224
257
  set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
225
- set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'httpServerOptions' ? http.ServerOptions | https.ServerOptions : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? cors.Options : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
258
+ set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'httpServerOptions' ? http.ServerOptions | https.ServerOptions : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? cors.Options : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : Opt extends 'redirectAllowedHosts' ? readonly string[] : never): void;
226
259
  override<Override extends keyof PsychicAppOverrides>(override: Override, value: PsychicAppOverrides[Override]): void;
227
260
  }
228
- export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'httpServerOptions' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
261
+ export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'httpServerOptions' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'redirectAllowedHosts' | 'routes' | 'saltRounds' | 'ssl';
229
262
  export interface PsychicAppSpecialHooks {
230
263
  cliSync: (() => any)[];
231
264
  serverInitBeforeMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
@@ -24,6 +24,42 @@ export default class Params {
24
24
  }> : Partial<{
25
25
  [K in ParamSafeColumns[number & keyof ParamSafeColumns] & string & keyof ParamSafeAttrs]: ParamSafeAttrs[K & keyof ParamSafeAttrs];
26
26
  }>, ReturnPayload extends ForOpts['array'] extends true ? ReturnPartialType[] : ReturnPartialType>(params: object, dreamClass: T, forOpts?: ForOpts): ReturnPayload;
27
+ /**
28
+ * ### .extract
29
+ *
30
+ * Typed + runtime-enforced allowlist extraction. Equivalent to
31
+ * {@link Params.for} with `only` moved to a required positional argument.
32
+ * Use from a controller via {@link PsychicController.extractParams}.
33
+ *
34
+ * The `allowed` array is constrained to `DreamParamSafeColumnNames<I>` at
35
+ * the type level, so protected columns (primary key, timestamps, belongs-to
36
+ * foreign keys, polymorphic type fields, STI `type` column, and any column
37
+ * in `explicitUnsafeParamColumns`) are TypeScript compile errors. At
38
+ * runtime, the existing intersection in `paramNamesForDreamClass` strips
39
+ * any column not also in the model's `paramSafeColumnsOrFallback()` set,
40
+ * so TS-bypass attempts still fail closed.
41
+ */
42
+ static extract<T extends typeof Dream, I extends InstanceType<T>, const AllowedArray extends readonly (keyof DreamParamSafeAttributes<I>)[], OptsType extends StrictInterface<OptsType, ExtractParamsOpts>, ParamSafeAttrs extends DreamParamSafeAttributes<I>, ReturnPartial extends Partial<{
43
+ [K in AllowedArray[number] & keyof ParamSafeAttrs]: ParamSafeAttrs[K & keyof ParamSafeAttrs];
44
+ }>, ReturnPayload extends OptsType['array'] extends true ? ReturnPartial[] : ReturnPartial>(params: object, dreamClass: T, allowed: AllowedArray, opts?: OptsType): ReturnPayload;
45
+ /**
46
+ * ### .extractImplicit
47
+ *
48
+ * Implicit-allowlist extraction driven by the model's `paramSafeColumns`
49
+ * declaration (or, when undeclared, the framework's default-safe fallback
50
+ * from `paramSafeColumnsOrFallback()`). Equivalent to {@link Params.for}
51
+ * without `only`. Use from a controller via
52
+ * {@link PsychicController.extractImplicitParams}.
53
+ *
54
+ * Prefer {@link Params.extract} when the caller can enumerate the allowed
55
+ * columns at the call site — it makes the allowlist visible to reviewers
56
+ * at the point of use. Reach for this method when the model-level
57
+ * `paramSafeColumns` declaration is the canonical allowlist and duplicating
58
+ * it at each call site would create maintenance drift.
59
+ */
60
+ static extractImplicit<T extends typeof Dream, I extends InstanceType<T>, OptsType extends StrictInterface<OptsType, ExtractParamsOpts>, ParamSafeColumnsOverride extends I['paramSafeColumns' & keyof I] extends never ? undefined : I['paramSafeColumns' & keyof I] & string[], ParamSafeColumns extends ParamSafeColumnsOverride extends string[] | Readonly<string[]> ? Extract<DreamParamSafeColumnNames<I>, ParamSafeColumnsOverride[number] & DreamParamSafeColumnNames<I>>[] : DreamParamSafeColumnNames<I>[], ParamSafeAttrs extends DreamParamSafeAttributes<I>, ReturnPartial extends Partial<{
61
+ [K in ParamSafeColumns[number & keyof ParamSafeColumns] & string]: ParamSafeAttrs[K & keyof ParamSafeAttrs];
62
+ }>, ReturnPayload extends OptsType['array'] extends true ? ReturnPartial[] : ReturnPartial>(params: object, dreamClass: T, opts?: OptsType): ReturnPayload;
27
63
  static restrict<T extends typeof Params>(this: T, params: PsychicParamsPrimitive | PsychicParamsDictionary | PsychicParamsDictionary[], allowed: string[]): PsychicParamsDictionary;
28
64
  /**
29
65
  * ### .cast
@@ -102,6 +138,16 @@ interface ParamsForOptsBase<OnlyArray> {
102
138
  export interface ParamsForOpts<OnlyArray> extends ParamsForOptsBase<OnlyArray> {
103
139
  key?: string;
104
140
  }
141
+ /**
142
+ * Options for {@link Params.extract} / {@link Params.extractImplicit} and the
143
+ * corresponding controller methods. Mirrors {@link ParamsForOpts} minus
144
+ * `only` — the explicit allowlist moved to a required positional argument on
145
+ * `extractParams`, and `extractImplicitParams` has no allowlist argument.
146
+ */
147
+ export interface ExtractParamsOpts {
148
+ array?: boolean;
149
+ key?: string;
150
+ }
105
151
  export interface OpenAPIDreamModelRequestBodyModifications<OnlyArray, IncludingArray> extends ParamsForOptsBase<OnlyArray> {
106
152
  combining?: OpenapiSchemaPropertiesShorthand;
107
153
  including?: IncludingArray;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "3.1.3",
5
+ "version": "3.2.1",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -75,7 +75,7 @@
75
75
  "@koa/cors": "^5.0.0",
76
76
  "@koa/etag": "^5.0.2",
77
77
  "@koa/router": "^15.3.0",
78
- "@rvoh/dream": "^2.7.0",
78
+ "@rvoh/dream": "^2.9.0",
79
79
  "@types/koa": "^3.0.1",
80
80
  "@types/koa-bodyparser": "^4.3.12",
81
81
  "@types/koa-conditional-get": "^2.0.3",
@@ -92,7 +92,7 @@
92
92
  "@koa/cors": "^5.0.0",
93
93
  "@koa/etag": "^5.0.2",
94
94
  "@koa/router": "^15.3.1",
95
- "@rvoh/dream": "^2.7.0",
95
+ "@rvoh/dream": "^2.9.0",
96
96
  "@rvoh/dream-spec-helpers": "^2.1.1",
97
97
  "@rvoh/psychic-spec-helpers": "3.0.0",
98
98
  "@types/koa": "^3.0.1",
@@ -141,7 +141,8 @@
141
141
  "packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402",
142
142
  "pnpm": {
143
143
  "overrides": {
144
- "diff": ">=8.0.3"
144
+ "diff": ">=8.0.3",
145
+ "path-to-regexp": ">=8.4.0"
145
146
  }
146
147
  }
147
148
  }