@hodeinavarro/odoo-rpc-ts 0.2.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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +401 -0
  3. package/dist/client.d.ts +184 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +277 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/combinators/retryOnSessionExpired.d.ts +13 -0
  8. package/dist/combinators/retryOnSessionExpired.d.ts.map +1 -0
  9. package/dist/combinators/retryOnSessionExpired.js +15 -0
  10. package/dist/combinators/retryOnSessionExpired.js.map +1 -0
  11. package/dist/commands.d.ts +91 -0
  12. package/dist/commands.d.ts.map +1 -0
  13. package/dist/commands.js +69 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +32 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +60 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/domain.d.ts +30 -0
  20. package/dist/domain.d.ts.map +1 -0
  21. package/dist/domain.js +70 -0
  22. package/dist/domain.js.map +1 -0
  23. package/dist/errors/auth.d.ts +27 -0
  24. package/dist/errors/auth.d.ts.map +1 -0
  25. package/dist/errors/auth.js +8 -0
  26. package/dist/errors/auth.js.map +1 -0
  27. package/dist/errors/index.d.ts +30 -0
  28. package/dist/errors/index.d.ts.map +1 -0
  29. package/dist/errors/index.js +13 -0
  30. package/dist/errors/index.js.map +1 -0
  31. package/dist/errors/mapFault.d.ts +31 -0
  32. package/dist/errors/mapFault.d.ts.map +1 -0
  33. package/dist/errors/mapFault.js +48 -0
  34. package/dist/errors/mapFault.js.map +1 -0
  35. package/dist/errors/protocol.d.ts +22 -0
  36. package/dist/errors/protocol.d.ts.map +1 -0
  37. package/dist/errors/protocol.js +9 -0
  38. package/dist/errors/protocol.js.map +1 -0
  39. package/dist/errors/schema.d.ts +22 -0
  40. package/dist/errors/schema.d.ts.map +1 -0
  41. package/dist/errors/schema.js +14 -0
  42. package/dist/errors/schema.js.map +1 -0
  43. package/dist/errors/server.d.ts +109 -0
  44. package/dist/errors/server.d.ts.map +1 -0
  45. package/dist/errors/server.js +49 -0
  46. package/dist/errors/server.js.map +1 -0
  47. package/dist/errors/session.d.ts +16 -0
  48. package/dist/errors/session.d.ts.map +1 -0
  49. package/dist/errors/session.js +11 -0
  50. package/dist/errors/session.js.map +1 -0
  51. package/dist/errors/transport.d.ts +42 -0
  52. package/dist/errors/transport.d.ts.map +1 -0
  53. package/dist/errors/transport.js +49 -0
  54. package/dist/errors/transport.js.map +1 -0
  55. package/dist/index.d.ts +33 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +57 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/internal/platform.d.ts +12 -0
  60. package/dist/internal/platform.d.ts.map +1 -0
  61. package/dist/internal/platform.js +11 -0
  62. package/dist/internal/platform.js.map +1 -0
  63. package/dist/internal/singleFlight.d.ts +29 -0
  64. package/dist/internal/singleFlight.d.ts.map +1 -0
  65. package/dist/internal/singleFlight.js +41 -0
  66. package/dist/internal/singleFlight.js.map +1 -0
  67. package/dist/profiles.d.ts +179 -0
  68. package/dist/profiles.d.ts.map +1 -0
  69. package/dist/profiles.js +191 -0
  70. package/dist/profiles.js.map +1 -0
  71. package/dist/protocol/jsonrpc.d.ts +127 -0
  72. package/dist/protocol/jsonrpc.d.ts.map +1 -0
  73. package/dist/protocol/jsonrpc.js +117 -0
  74. package/dist/protocol/jsonrpc.js.map +1 -0
  75. package/dist/records/index.d.ts +14 -0
  76. package/dist/records/index.d.ts.map +1 -0
  77. package/dist/records/index.js +14 -0
  78. package/dist/records/index.js.map +1 -0
  79. package/dist/records/recordModel.d.ts +131 -0
  80. package/dist/records/recordModel.d.ts.map +1 -0
  81. package/dist/records/recordModel.js +101 -0
  82. package/dist/records/recordModel.js.map +1 -0
  83. package/dist/records/related.d.ts +42 -0
  84. package/dist/records/related.d.ts.map +1 -0
  85. package/dist/records/related.js +51 -0
  86. package/dist/records/related.js.map +1 -0
  87. package/dist/records/relations.d.ts +66 -0
  88. package/dist/records/relations.d.ts.map +1 -0
  89. package/dist/records/relations.js +119 -0
  90. package/dist/records/relations.js.map +1 -0
  91. package/dist/records/spec.d.ts +44 -0
  92. package/dist/records/spec.d.ts.map +1 -0
  93. package/dist/records/spec.js +63 -0
  94. package/dist/records/spec.js.map +1 -0
  95. package/dist/records/typed.d.ts +71 -0
  96. package/dist/records/typed.d.ts.map +1 -0
  97. package/dist/records/typed.js +65 -0
  98. package/dist/records/typed.js.map +1 -0
  99. package/dist/rpc.d.ts +90 -0
  100. package/dist/rpc.d.ts.map +1 -0
  101. package/dist/rpc.js +113 -0
  102. package/dist/rpc.js.map +1 -0
  103. package/dist/services/db.d.ts +128 -0
  104. package/dist/services/db.d.ts.map +1 -0
  105. package/dist/services/db.js +132 -0
  106. package/dist/services/db.js.map +1 -0
  107. package/dist/services/report.d.ts +48 -0
  108. package/dist/services/report.d.ts.map +1 -0
  109. package/dist/services/report.js +103 -0
  110. package/dist/services/report.js.map +1 -0
  111. package/dist/session/cookie.d.ts +114 -0
  112. package/dist/session/cookie.d.ts.map +1 -0
  113. package/dist/session/cookie.js +231 -0
  114. package/dist/session/cookie.js.map +1 -0
  115. package/dist/testing/fakeTransport.d.ts +40 -0
  116. package/dist/testing/fakeTransport.d.ts.map +1 -0
  117. package/dist/testing/fakeTransport.js +36 -0
  118. package/dist/testing/fakeTransport.js.map +1 -0
  119. package/dist/testing/index.d.ts +9 -0
  120. package/dist/testing/index.d.ts.map +1 -0
  121. package/dist/testing/index.js +9 -0
  122. package/dist/testing/index.js.map +1 -0
  123. package/dist/transport.d.ts +62 -0
  124. package/dist/transport.d.ts.map +1 -0
  125. package/dist/transport.js +9 -0
  126. package/dist/transport.js.map +1 -0
  127. package/dist/transports/json2.d.ts +41 -0
  128. package/dist/transports/json2.d.ts.map +1 -0
  129. package/dist/transports/json2.js +196 -0
  130. package/dist/transports/json2.js.map +1 -0
  131. package/dist/transports/jsonrpc.d.ts +41 -0
  132. package/dist/transports/jsonrpc.d.ts.map +1 -0
  133. package/dist/transports/jsonrpc.js +119 -0
  134. package/dist/transports/jsonrpc.js.map +1 -0
  135. package/dist/transports/web.d.ts +15 -0
  136. package/dist/transports/web.d.ts.map +1 -0
  137. package/dist/transports/web.js +62 -0
  138. package/dist/transports/web.js.map +1 -0
  139. package/dist/version-live.d.ts +41 -0
  140. package/dist/version-live.d.ts.map +1 -0
  141. package/dist/version-live.js +26 -0
  142. package/dist/version-live.js.map +1 -0
  143. package/dist/version.d.ts +78 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +46 -0
  146. package/dist/version.js.map +1 -0
  147. package/docs/protocol-verification.md +43 -0
  148. package/package.json +71 -0
  149. package/src/client.ts +731 -0
  150. package/src/combinators/retryOnSessionExpired.ts +25 -0
  151. package/src/commands.ts +112 -0
  152. package/src/config.ts +110 -0
  153. package/src/domain.ts +91 -0
  154. package/src/errors/auth.ts +28 -0
  155. package/src/errors/index.ts +50 -0
  156. package/src/errors/mapFault.ts +92 -0
  157. package/src/errors/protocol.ts +19 -0
  158. package/src/errors/schema.ts +18 -0
  159. package/src/errors/server.ts +79 -0
  160. package/src/errors/session.ts +12 -0
  161. package/src/errors/transport.ts +74 -0
  162. package/src/index.ts +152 -0
  163. package/src/internal/platform.ts +22 -0
  164. package/src/internal/singleFlight.ts +80 -0
  165. package/src/profiles.ts +386 -0
  166. package/src/protocol/jsonrpc.ts +162 -0
  167. package/src/records/index.ts +49 -0
  168. package/src/records/recordModel.ts +211 -0
  169. package/src/records/related.ts +80 -0
  170. package/src/records/relations.ts +192 -0
  171. package/src/records/spec.ts +96 -0
  172. package/src/records/typed.ts +180 -0
  173. package/src/rpc.ts +219 -0
  174. package/src/services/db.ts +282 -0
  175. package/src/services/report.ts +165 -0
  176. package/src/session/cookie.ts +461 -0
  177. package/src/testing/fakeTransport.ts +84 -0
  178. package/src/testing/index.ts +9 -0
  179. package/src/transport.ts +72 -0
  180. package/src/transports/json2.ts +303 -0
  181. package/src/transports/jsonrpc.ts +218 -0
  182. package/src/transports/web.ts +96 -0
  183. package/src/version-live.ts +67 -0
  184. package/src/version.ts +102 -0
@@ -0,0 +1,103 @@
1
+ import { Effect, Result, Schema } from "effect";
2
+ import { OdooAccessError, OdooMissingError } from "../errors/server.js";
3
+ import { SchemaDriftError } from "../errors/schema.js";
4
+ import { OdooTransportError } from "../errors/transport.js";
5
+ /**
6
+ * Lenient decoder for an `ir.actions.report` row. Only the columns we request
7
+ * are decoded; extra keys are tolerated. `report_name` is the identifier that
8
+ * feeds the `GET /report/<converter>/<report_name>/<ids>` download route.
9
+ */
10
+ const ReportActionSchema = Schema.Struct({
11
+ id: Schema.Number,
12
+ name: Schema.String,
13
+ report_name: Schema.String,
14
+ model: Schema.String,
15
+ report_type: Schema.String,
16
+ });
17
+ const toReportAction = (row) => ({
18
+ id: row.id,
19
+ name: row.name,
20
+ reportName: row.report_name,
21
+ model: row.model,
22
+ reportType: row.report_type,
23
+ });
24
+ /**
25
+ * List `ir.actions.report` definitions over a cookie session, optionally scoped
26
+ * to a single `model`. Self-contained: goes through the session's `json` hatch
27
+ * (`web/dataset/call_kw` → `search_read`) so it needs no `Transport`/`Rpc`
28
+ * wiring. Fails with the session's `CookieLoginError` union (login faults,
29
+ * server faults, or `SchemaDriftError` on a drifted row shape).
30
+ */
31
+ export const list = (session, model) => Effect.gen(function* () {
32
+ const domain = model === undefined ? [] : [["model", "=", model]];
33
+ const result = yield* session.json("web/dataset/call_kw", {
34
+ model: "ir.actions.report",
35
+ method: "search_read",
36
+ args: [domain, ["id", "name", "report_name", "model", "report_type"]],
37
+ kwargs: {},
38
+ });
39
+ const rows = yield* Schema.decodeUnknownEffect(Schema.Array(ReportActionSchema))(result).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "ir.actions.report rows", payload: result, cause })));
40
+ return rows.map(toReportAction);
41
+ });
42
+ /** The `%PDF` signature every well-formed PDF opens with. */
43
+ const PDF_MAGIC = Uint8Array.of(0x25, 0x50, 0x44, 0x46);
44
+ const hasPdfMagic = (bytes) => bytes.length >= PDF_MAGIC.length && PDF_MAGIC.every((byte, i) => bytes[i] === byte);
45
+ /**
46
+ * Download a rendered report as raw bytes via `GET
47
+ * /report/<converter>/<reportName>/<ids.join(",")>` on the cookie-bound client
48
+ * (the route is identical and CSRF-free across Odoo 16–19).
49
+ *
50
+ * Status mapping: `404 → OdooMissingError`, `403 → OdooAccessError`, any other
51
+ * non-2xx (including the `500` Odoo raises for an unknown report name) →
52
+ * `OdooTransportError`. For `converter === "pdf"` the 2xx body is checked for
53
+ * the `%PDF` magic and fails as `SchemaDriftError` if the server handed back
54
+ * something else (e.g. an HTML error page with a 200).
55
+ */
56
+ export const download = (session, options) => Effect.gen(function* () {
57
+ const converter = options.converter ?? "pdf";
58
+ const path = `report/${converter}/${options.reportName}/${options.ids.join(",")}`;
59
+ const response = yield* session.http({ path, method: "GET" });
60
+ const request = { method: "GET", url: response.request.url };
61
+ if (response.status === 404) {
62
+ return yield* Effect.fail(new OdooMissingError({
63
+ name: "werkzeug.exceptions.NotFound",
64
+ message: `Report ${options.reportName} not found (404) for ids ${options.ids.join(",")}.`,
65
+ arguments: [],
66
+ context: {},
67
+ }));
68
+ }
69
+ if (response.status === 403) {
70
+ return yield* Effect.fail(new OdooAccessError({
71
+ name: "werkzeug.exceptions.Forbidden",
72
+ message: `Access denied (403) downloading report ${options.reportName}.`,
73
+ arguments: [],
74
+ context: {},
75
+ }));
76
+ }
77
+ if (response.status < 200 || response.status >= 300) {
78
+ return yield* Effect.fail(new OdooTransportError({
79
+ request,
80
+ kind: "StatusCodeError",
81
+ status: response.status,
82
+ }));
83
+ }
84
+ const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
85
+ const bytes = new Uint8Array(buffer);
86
+ if (converter === "pdf" && !hasPdfMagic(bytes)) {
87
+ // Manufacture a real ParseError so SchemaDriftError.cause is honest: a 2xx
88
+ // body that is not a PDF (e.g. an HTML error page served with status 200).
89
+ const header = new TextDecoder().decode(bytes.subarray(0, PDF_MAGIC.length));
90
+ const parsed = Schema.decodeUnknownResult(Schema.Literal("%PDF"))(header);
91
+ if (Result.isFailure(parsed)) {
92
+ return yield* Effect.fail(new SchemaDriftError({
93
+ context: `report/pdf/${options.reportName}`,
94
+ payload: `non-PDF body (${bytes.length} bytes, status ${response.status})`,
95
+ cause: parsed.failure,
96
+ }));
97
+ }
98
+ }
99
+ return bytes;
100
+ });
101
+ /** The report service, grouped for `ReportService.list` / `ReportService.download`. */
102
+ export const ReportService = { list, download };
103
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/services/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import { type Config, Context, Effect, Layer, Option, Redacted, Ref } from "effect";
2
+ import { OdooConfig } from "../config.js";
3
+ import { OdooAuthenticationError } from "../errors/auth.js";
4
+ import type { OdooServerFault } from "../errors/server.js";
5
+ import { SchemaDriftError } from "../errors/schema.js";
6
+ import { SessionExpiredError } from "../errors/session.js";
7
+ import { OdooTransportError } from "../errors/transport.js";
8
+ import { Cookies, HttpClient, HttpClientResponse } from "../internal/platform.js";
9
+ /**
10
+ * Join a request path onto the configured base URL, preserving any base path
11
+ * (e.g. a server mounted under `/odoo`). Keeping this here means both the
12
+ * authenticate call and the `call_kw` transport derive endpoints identically.
13
+ */
14
+ export declare const joinPath: (base: URL, path: string) => string;
15
+ /** A live, authenticated web session. `uid` is always a real user id here. */
16
+ export interface OdooSessionInfo {
17
+ readonly uid: number;
18
+ readonly userContext: Record<string, unknown>;
19
+ readonly serverVersionInfo: unknown;
20
+ /** The full, undecoded `session_info` result for downstream passthrough. */
21
+ readonly raw: Record<string, unknown>;
22
+ }
23
+ /**
24
+ * The typed failure channel of a login round trip. `SessionExpiredError` is in
25
+ * the union only because it flows through the shared `mapJsonRpcError`; a fresh
26
+ * authenticate should never actually produce it.
27
+ */
28
+ export type CookieLoginError = OdooTransportError | SchemaDriftError | OdooAuthenticationError | OdooServerFault | SessionExpiredError;
29
+ /**
30
+ * The cookie-session service. Owns the `session_id` cookie jar and a
31
+ * success-only single-flight around `/web/session/authenticate`.
32
+ *
33
+ * `client` is the HttpClient decorated with the shared cookie `Ref` — the web
34
+ * transport MUST reuse it so the login `Set-Cookie` (and any mid-session
35
+ * rotation) is carried on every subsequent `call_kw`.
36
+ */
37
+ /**
38
+ * Options for {@link CookieSessionService.http}: a raw HTTP round trip on the
39
+ * cookie-bound client. `path` is joined onto the session's base url (a base
40
+ * mount like `/odoo` is preserved). `method` defaults to `GET`.
41
+ */
42
+ export interface RawHttpOptions {
43
+ readonly path: string;
44
+ readonly method?: string;
45
+ readonly body?: Uint8Array;
46
+ readonly headers?: Record<string, string>;
47
+ }
48
+ export interface CookieSessionService {
49
+ /** Ensure a login has run and return the live session. Single-flight. */
50
+ readonly login: Effect.Effect<OdooSessionInfo, CookieLoginError>;
51
+ /**
52
+ * Raw JSON-RPC escape hatch: ensure login, then POST the shared JSON-RPC
53
+ * envelope (`{jsonrpc, method:"call", params, id}`) to an arbitrary `path`
54
+ * under the session's base url and return the raw `result` member. Server
55
+ * faults flow through the shared `mapJsonRpcError` choke point (code 100 →
56
+ * `SessionExpiredError`, etc.); a non-envelope body fails as `SchemaDriftError`.
57
+ * For calling models this is `web/dataset/call_kw` with the `{model, method,
58
+ * args, kwargs}` params shape.
59
+ */
60
+ readonly json: (path: string, params?: Record<string, unknown>) => Effect.Effect<unknown, CookieLoginError>;
61
+ /**
62
+ * Raw HTTP escape hatch: ensure login, then issue a raw request on the
63
+ * cookie-bound client and return the response UNTOUCHED. The caller owns the
64
+ * response fully, including any non-2xx status — this hatch never inspects it
65
+ * and only fails (`OdooTransportError`) on a wire-level error or a failed
66
+ * login. Used for binary endpoints like `GET /report/<converter>/...`.
67
+ */
68
+ readonly http: (options: RawHttpOptions) => Effect.Effect<HttpClientResponse.HttpClientResponse, CookieLoginError>;
69
+ /**
70
+ * Drop the cached session and the (now-dead) `session_id` cookie so the next
71
+ * `login` re-authenticates. Does NOT auto-relogin — that is the opt-in
72
+ * `retryOnSessionExpired` combinator's job.
73
+ */
74
+ readonly invalidate: Effect.Effect<void>;
75
+ /** The shared cookie jar. Exposed for the transport's `withCookiesRef`. */
76
+ readonly cookies: Ref.Ref<Cookies.Cookies>;
77
+ /** HttpClient bound to the shared cookie jar; the transport reuses this. */
78
+ readonly client: HttpClient.HttpClient;
79
+ /** Peek at the current session without triggering a login (observability). */
80
+ readonly peek: Effect.Effect<Option.Option<OdooSessionInfo>>;
81
+ }
82
+ declare const CookieSession_base: Context.ServiceClass<CookieSession, "odoo-rpc-ts/CookieSession", CookieSessionService>;
83
+ export declare class CookieSession extends CookieSession_base {
84
+ }
85
+ export declare const make: (config: OdooConfig) => Effect.Effect<CookieSessionService, never, HttpClient.HttpClient>;
86
+ /** Provide `CookieSession` from an already-resolved config. Requires `HttpClient`. */
87
+ export declare const layer: (config: OdooConfig) => Layer.Layer<CookieSession, never, HttpClient.HttpClient>;
88
+ /** Provide `CookieSession`, resolving `OdooConfig` from the environment. */
89
+ export declare const layerConfig: Layer.Layer<CookieSession, Config.ConfigError, HttpClient.HttpClient>;
90
+ /**
91
+ * Options for {@link fromExisting}: adopt a `session_id` cookie that was
92
+ * minted elsewhere (e.g. harvested from an embedded browser window after the
93
+ * user completed the real `/web/login` page — the only stock flow that works
94
+ * for TOTP/SSO accounts).
95
+ */
96
+ export interface ExistingSessionOptions {
97
+ /** Base server URL (same shape as `OdooConfig.url`; TLS rules apply). */
98
+ readonly url: URL;
99
+ /** The harvested `session_id` cookie value. Redacted until the wire. */
100
+ readonly sessionId: Redacted.Redacted<string>;
101
+ /**
102
+ * Optional renewal hook: an Effect that yields a FRESH `session_id` (e.g.
103
+ * by driving a login window again). Runs inside the login single-flight
104
+ * after an `invalidate`, so concurrent callers trigger it at most once.
105
+ * Must be fully provided (`R = never`) and fail within `CookieLoginError` —
106
+ * map UI-level failures (window closed, …) into `OdooAuthenticationError`.
107
+ */
108
+ readonly renew?: Effect.Effect<Redacted.Redacted<string>, CookieLoginError>;
109
+ }
110
+ export declare const fromExisting: (options: ExistingSessionOptions) => Effect.Effect<CookieSessionService, never, HttpClient.HttpClient>;
111
+ /** Provide `CookieSession` from a harvested cookie. See {@link fromExisting}. */
112
+ export declare const layerFromExisting: (options: ExistingSessionOptions) => Layer.Layer<CookieSession, never, HttpClient.HttpClient>;
113
+ export {};
114
+ //# sourceMappingURL=cookie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../../src/session/cookie.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,231 @@
1
+ import { Context, Effect, Layer, Option, Redacted, Ref, Schema } from "effect";
2
+ import { OdooConfig } from "../config.js";
3
+ import { OdooAuthenticationError } from "../errors/auth.js";
4
+ import { SchemaDriftError } from "../errors/schema.js";
5
+ import { SessionExpiredError } from "../errors/session.js";
6
+ import { OdooTransportError } from "../errors/transport.js";
7
+ import { Cookies, HttpClient, HttpClientRequest, HttpClientResponse, } from "../internal/platform.js";
8
+ import * as SingleFlight from "../internal/singleFlight.js";
9
+ import { buildRequest, JsonRpcResponse, mapJsonRpcError, nextRequestId, } from "../protocol/jsonrpc.js";
10
+ /**
11
+ * Join a request path onto the configured base URL, preserving any base path
12
+ * (e.g. a server mounted under `/odoo`). Keeping this here means both the
13
+ * authenticate call and the `call_kw` transport derive endpoints identically.
14
+ */
15
+ export const joinPath = (base, path) => {
16
+ const href = base.href.endsWith("/") ? base.href : `${base.href}/`;
17
+ return new URL(path, href).href;
18
+ };
19
+ /**
20
+ * `session_info` as returned by `/web/session/authenticate`. Decoded leniently:
21
+ * `uid` is `null` while a second factor is still pending, extra keys are
22
+ * tolerated, and `server_version_info` passes through undecoded.
23
+ */
24
+ const SessionInfoSchema = Schema.Struct({
25
+ uid: Schema.NullOr(Schema.Number),
26
+ user_context: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
27
+ server_version_info: Schema.optional(Schema.Unknown),
28
+ });
29
+ export class CookieSession extends Context.Service()("odoo-rpc-ts/CookieSession") {
30
+ }
31
+ /**
32
+ * One session-info round trip: POST a JSON-RPC envelope, decode the response,
33
+ * map faults through the shared choke point (code 100 → `SessionExpiredError`),
34
+ * and validate the returned `session_info`. Shared by the credential login and
35
+ * the injected-cookie path so both decode — and fail — identically.
36
+ */
37
+ const sessionInfoRoundTrip = (client, url, params, endpoint, method) => Effect.gen(function* () {
38
+ const request = { method: "POST", url };
39
+ const envelope = buildRequest(params, nextRequestId());
40
+ const response = yield* client
41
+ .execute(HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(url), envelope))
42
+ .pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
43
+ const body = yield* response.json.pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
44
+ const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(body).pipe(Effect.mapError((cause) => new SchemaDriftError({
45
+ context: `${endpoint} envelope`,
46
+ payload: body,
47
+ cause,
48
+ })));
49
+ if ("error" in decoded) {
50
+ return yield* Effect.fail(mapJsonRpcError(decoded.error, { method }));
51
+ }
52
+ const info = yield* Schema.decodeUnknownEffect(SessionInfoSchema)(decoded.result).pipe(Effect.mapError((cause) => new SchemaDriftError({
53
+ context: `${endpoint} session_info`,
54
+ payload: decoded.result,
55
+ cause,
56
+ })));
57
+ if (info.uid === null) {
58
+ return yield* Effect.fail(new OdooAuthenticationError({
59
+ reason: "mfa-pending",
60
+ message: "Login accepted but a second factor is required before a session exists.",
61
+ }));
62
+ }
63
+ const session = {
64
+ uid: info.uid,
65
+ userContext: info.user_context ?? {},
66
+ serverVersionInfo: info.server_version_info,
67
+ raw: typeof decoded.result === "object" && decoded.result !== null
68
+ ? decoded.result
69
+ : {},
70
+ };
71
+ return session;
72
+ });
73
+ /**
74
+ * Build the two raw escape hatches (`json`, `http`) shared by {@link make} and
75
+ * {@link fromExisting}. Both close over the SAME cookie-bound `client`, base
76
+ * `url`, and single-flight `login` the rest of the service uses, so a hatch
77
+ * request rides the live `session_id` cookie exactly like a `call_kw` does.
78
+ */
79
+ const makeRawHatches = (client, baseUrl, login) => {
80
+ const json = (path, params = {}) => Effect.gen(function* () {
81
+ yield* login;
82
+ const url = joinPath(baseUrl, path);
83
+ const request = { method: "POST", url };
84
+ const envelope = buildRequest(params, nextRequestId());
85
+ const response = yield* client
86
+ .execute(HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(url), envelope))
87
+ .pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
88
+ const body = yield* response.json.pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
89
+ const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(body).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: `${path} envelope`, payload: body, cause })));
90
+ if ("error" in decoded) {
91
+ return yield* Effect.fail(mapJsonRpcError(decoded.error, { method: path }));
92
+ }
93
+ return decoded.result;
94
+ });
95
+ const http = (options) => Effect.gen(function* () {
96
+ yield* login;
97
+ const url = joinPath(baseUrl, options.path);
98
+ const method = options.method ?? "GET";
99
+ const request = { method, url };
100
+ let req = HttpClientRequest.make(method)(url);
101
+ if (options.headers !== undefined) {
102
+ req = HttpClientRequest.setHeaders(req, options.headers);
103
+ }
104
+ if (options.body !== undefined) {
105
+ req = HttpClientRequest.bodyUint8Array(req, options.body);
106
+ }
107
+ // Response returned untouched — the caller owns any non-2xx status.
108
+ return yield* client
109
+ .execute(req)
110
+ .pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
111
+ });
112
+ return { json, http };
113
+ };
114
+ export const make = (config) => Effect.gen(function* () {
115
+ const baseClient = yield* HttpClient.HttpClient;
116
+ const cookies = yield* Ref.make(Cookies.empty);
117
+ const client = HttpClient.withCookiesRef(baseClient, cookies);
118
+ const authUrl = joinPath(config.url, "web/session/authenticate");
119
+ const doLogin = Effect.suspend(() => {
120
+ // Redacted.value only at the wire boundary; for ApiKey creds the key is
121
+ // sent as the password (valid on all supported Odoo versions).
122
+ const password = config.credentials._tag === "ApiKey"
123
+ ? Redacted.value(config.credentials.apiKey)
124
+ : Redacted.value(config.credentials.password);
125
+ return sessionInfoRoundTrip(client, authUrl, { db: config.db, login: config.credentials.username, password }, "web/session/authenticate", "authenticate");
126
+ });
127
+ // The flight owns both caching and invalidation. Its generation counter
128
+ // guarantees a login already in flight when `invalidate` runs cannot
129
+ // resurrect the cache with a stale session (its caller still gets it).
130
+ const flight = yield* SingleFlight.make(doLogin);
131
+ const invalidate = Effect.gen(function* () {
132
+ yield* Ref.set(cookies, Cookies.empty);
133
+ yield* flight.invalidate;
134
+ });
135
+ return {
136
+ login: flight.get,
137
+ invalidate,
138
+ cookies,
139
+ client,
140
+ peek: flight.peek,
141
+ ...makeRawHatches(client, config.url, flight.get),
142
+ };
143
+ });
144
+ /** Provide `CookieSession` from an already-resolved config. Requires `HttpClient`. */
145
+ export const layer = (config) => Layer.effect(CookieSession, make(config));
146
+ /** Provide `CookieSession`, resolving `OdooConfig` from the environment. */
147
+ export const layerConfig = Layer.effect(CookieSession, Effect.flatMap(OdooConfig, make));
148
+ /**
149
+ * Build a `CookieSessionService` from an already-minted `session_id` cookie —
150
+ * no credentials are ever held. `login` seeds the cookie jar and validates the
151
+ * session via `POST /web/session/get_session_info` (verified 16–19: JSON-RPC
152
+ * envelope, `auth="user"`), so `OdooSessionInfo` (`uid`, context, version) is
153
+ * always real, never fabricated. Success-only single-flight, like {@link make}.
154
+ *
155
+ * Expired-session contract: this session cannot re-login by itself. When the
156
+ * server kills the session, calls fail with `SessionExpiredError`; after
157
+ * `invalidate`, a subsequent `login` fails FAST with `SessionExpiredError`
158
+ * (no network) unless a `renew` hook was provided. `retryOnSessionExpired`
159
+ * therefore performs exactly one relogin attempt and propagates the second
160
+ * `SessionExpiredError` — it can never spin. The consumer's shell is expected
161
+ * to catch it, re-run its login UI, and construct a fresh session (or supply
162
+ * `renew` to do the same in place).
163
+ */
164
+ // RFC 6265 `cookie-octet`: the exact charset a cookie value may put on the
165
+ // wire (excludes controls, whitespace, DQUOTE, comma, semicolon, backslash).
166
+ const cookieOctets = /^[\u0021\u0023-\u002b\u002d-\u003a\u003c-\u005b\u005d-\u007e]+$/;
167
+ export const fromExisting = (options) => Effect.gen(function* () {
168
+ const baseClient = yield* HttpClient.HttpClient;
169
+ const cookies = yield* Ref.make(Cookies.empty);
170
+ const client = HttpClient.withCookiesRef(baseClient, cookies);
171
+ const infoUrl = joinPath(options.url, "web/session/get_session_info");
172
+ // The cookie to seed on the next login. Starts as the harvested value;
173
+ // consumed once, and cleared by `invalidate` so a dead cookie is never
174
+ // re-seeded. After that only `renew` can produce another one.
175
+ const nextCookie = yield* Ref.make(Option.some(options.sessionId));
176
+ // Redacted.value only at the wire boundary (the cookie jar IS the wire
177
+ // buffer). Seeded via `fromSetCookie` so the raw value rides the wire
178
+ // untouched, exactly like a server-set cookie — `Cookies.makeCookie`
179
+ // would percent-encode it (corrupting `+`/`=`/`/`), and an unvalidated
180
+ // `fromSetCookie` would silently truncate at the first `;`.
181
+ const seed = (value) => {
182
+ const raw = Redacted.value(value);
183
+ if (raw === "" || !cookieOctets.test(raw)) {
184
+ return Effect.fail(new OdooAuthenticationError({
185
+ reason: "invalid-credentials",
186
+ message: "The provided session_id is not a valid cookie value (RFC 6265).",
187
+ }));
188
+ }
189
+ return Ref.update(cookies, (jar) => Cookies.merge(jar, Cookies.fromSetCookie(`session_id=${raw}; Path=/`)));
190
+ };
191
+ const doLogin = Effect.gen(function* () {
192
+ const jar = yield* Ref.get(cookies);
193
+ if (Option.isNone(Cookies.getValue(jar, "session_id"))) {
194
+ const pending = yield* Ref.getAndSet(nextCookie, Option.none());
195
+ if (Option.isSome(pending)) {
196
+ yield* seed(pending.value);
197
+ }
198
+ else if (options.renew !== undefined) {
199
+ yield* seed(yield* options.renew);
200
+ }
201
+ else {
202
+ // Fail FAST, without a round trip: this session holds no credentials
203
+ // and no renew hook, so re-login is structurally impossible.
204
+ return yield* Effect.fail(new SessionExpiredError({
205
+ message: "The injected web session was invalidated and no renew hook was provided; " +
206
+ "obtain a fresh session_id and construct a new session.",
207
+ }));
208
+ }
209
+ }
210
+ // Validate + hydrate: a dead cookie surfaces here as code 100 →
211
+ // SessionExpiredError via the shared choke point.
212
+ return yield* sessionInfoRoundTrip(client, infoUrl, {}, "web/session/get_session_info", "get_session_info");
213
+ });
214
+ const flight = yield* SingleFlight.make(doLogin);
215
+ const invalidate = Effect.gen(function* () {
216
+ yield* Ref.set(cookies, Cookies.empty);
217
+ yield* Ref.set(nextCookie, Option.none());
218
+ yield* flight.invalidate;
219
+ });
220
+ return {
221
+ login: flight.get,
222
+ invalidate,
223
+ cookies,
224
+ client,
225
+ peek: flight.peek,
226
+ ...makeRawHatches(client, options.url, flight.get),
227
+ };
228
+ });
229
+ /** Provide `CookieSession` from a harvested cookie. See {@link fromExisting}. */
230
+ export const layerFromExisting = (options) => Layer.effect(CookieSession, fromExisting(options));
231
+ //# sourceMappingURL=cookie.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie.js","sourceRoot":"","sources":["../../src/session/cookie.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { Effect, Layer, Ref } from "effect";
2
+ import { type CallKwParams, Transport, type TransportCallError, type TransportDialect } from "../transport.js";
3
+ /**
4
+ * A single scripted method. Receives the full {@link CallKwParams} of the
5
+ * `call_kw` — `args`, `kwargs`, and the browse `ids` faithfully, because the
6
+ * seam is the contract, not a projection of it — and returns either a bare value
7
+ * (wrapped in `Effect.succeed`) or an `Effect`; the latter lets a handler script
8
+ * a typed transport failure.
9
+ */
10
+ export type FakeHandler = (params: CallKwParams) => Effect.Effect<unknown, TransportCallError> | unknown;
11
+ /**
12
+ * Scripted responses keyed by `model` then `method`. Anything not scripted is a
13
+ * test misconfiguration — see the dispatch note in {@link make}.
14
+ */
15
+ export type FakeHandlers = {
16
+ readonly [model: string]: {
17
+ readonly [method: string]: FakeHandler;
18
+ };
19
+ };
20
+ /** A `FakeTransport` layer paired with the log of every call it received. */
21
+ export interface FakeTransport {
22
+ readonly layer: Layer.Layer<Transport>;
23
+ /**
24
+ * Every `call_kw` this transport handled, in order, for assertions. Populated
25
+ * before the handler runs, so a failing handler still records its call.
26
+ */
27
+ readonly callLog: Ref.Ref<ReadonlyArray<CallKwParams>>;
28
+ }
29
+ /**
30
+ * Build a deterministic, network-free {@link Transport} from scripted handlers.
31
+ *
32
+ * GOTCHA: an unhandled model/method is a defect in the test, not a runtime
33
+ * condition the code under test could ever encounter — so it is an `Effect.die`
34
+ * (with a precise locator), never a value in the typed failure channel. Tests
35
+ * that mean to exercise a transport failure must script it explicitly.
36
+ */
37
+ export declare const make: (handlers: FakeHandlers, options?: {
38
+ readonly dialect?: TransportDialect;
39
+ }) => FakeTransport;
40
+ //# sourceMappingURL=fakeTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeTransport.d.ts","sourceRoot":"","sources":["../../src/testing/fakeTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EACL,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,MAAM,EAAE,YAAY,KACjB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,kBAAkB,CAAC,GAAG,OAAO,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG;QACxB,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC;CACxD;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,aACL,YAAY,YACZ;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAAE,KAChD,aA6BF,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { Effect, Layer, Ref } from "effect";
2
+ import { Transport, } from "../transport.js";
3
+ const isEffect = (value) => Effect.isEffect(value);
4
+ /**
5
+ * Build a deterministic, network-free {@link Transport} from scripted handlers.
6
+ *
7
+ * GOTCHA: an unhandled model/method is a defect in the test, not a runtime
8
+ * condition the code under test could ever encounter — so it is an `Effect.die`
9
+ * (with a precise locator), never a value in the typed failure channel. Tests
10
+ * that mean to exercise a transport failure must script it explicitly.
11
+ */
12
+ export const make = (handlers, options) => {
13
+ const callLog = Ref.makeUnsafe([]);
14
+ const layer = Layer.succeed(Transport, {
15
+ dialect: options?.dialect ?? "execute-kw",
16
+ callKw: (params) => Effect.gen(function* () {
17
+ const handler = handlers[params.model]?.[params.method];
18
+ if (handler === undefined) {
19
+ // Every real Odoo server answers the seeded default layer's
20
+ // res.users.context_get, so the fake does too — WITHOUT logging it
21
+ // (it is ambient plumbing, and logging it would shift positional
22
+ // log assertions). Script res.users.context_get to observe/override.
23
+ if (params.model === "res.users" && params.method === "context_get") {
24
+ return {};
25
+ }
26
+ yield* Ref.update(callLog, (log) => [...log, params]);
27
+ return yield* Effect.die(`FakeTransport: no handler for ${params.model}.${params.method}`);
28
+ }
29
+ yield* Ref.update(callLog, (log) => [...log, params]);
30
+ const result = handler(params);
31
+ return isEffect(result) ? yield* result : result;
32
+ }),
33
+ });
34
+ return { layer, callLog };
35
+ };
36
+ //# sourceMappingURL=fakeTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fakeTransport.js","sourceRoot":"","sources":["../../src/testing/fakeTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAEL,SAAS,GAGV,MAAM,iBAAiB,CAAC;AAiCzB,MAAM,QAAQ,GAAG,CACf,KAA2D,EACN,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAEjF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,QAAsB,EACtB,OAAiD,EAClC,EAAE;IACjB,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAA8B,EAAE,CAAC,CAAC;IAEhE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE;QACrC,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,YAAY;QACzC,MAAM,EAAE,CAAC,MAAoB,EAAE,EAAE,CAC/B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACxD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,4DAA4D;gBAC5D,mEAAmE;gBACnE,iEAAiE;gBACjE,qEAAqE;gBACrE,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,IAAI,MAAM,CAAC,MAAM,KAAK,aAAa,EAAE,CAAC;oBACpE,OAAO,EAAE,CAAC;gBACZ,CAAC;gBACD,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;gBACtD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CACtB,iCAAiC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CACjE,CAAC;YACJ,CAAC;YACD,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YAEtD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;YAC/B,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACnD,CAAC,CAAC;KACL,CAAC,CAAC;IAEH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5B,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @hodeinavarro/odoo-rpc-ts/testing — deterministic test doubles.
3
+ *
4
+ * `FakeTransport` is a scripted, network-free {@link Transport} implementation
5
+ * (AGENTS.md § Testing export). Compose its `.layer` under the service you want
6
+ * to exercise and assert against its `.callLog`.
7
+ */
8
+ export { make, type FakeHandler, type FakeHandlers, type FakeTransport } from "./fakeTransport.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @hodeinavarro/odoo-rpc-ts/testing — deterministic test doubles.
3
+ *
4
+ * `FakeTransport` is a scripted, network-free {@link Transport} implementation
5
+ * (AGENTS.md § Testing export). Compose its `.layer` under the service you want
6
+ * to exercise and assert against its `.callLog`.
7
+ */
8
+ export { make } from "./fakeTransport.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAA2D,MAAM,oBAAoB,CAAC"}