@sasjs/api 0.0.0-stage → 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 (49) hide show
  1. package/LICENCE.md +21 -0
  2. package/README.md +87 -2
  3. package/build/browser.js +2469 -0
  4. package/build/cjs/corpus/drive.js +288 -0
  5. package/build/cjs/corpus/runner.js +35 -0
  6. package/build/cjs/drive/memory-store.js +126 -0
  7. package/build/cjs/drive/paths.js +82 -0
  8. package/build/cjs/drive/service.js +143 -0
  9. package/build/cjs/host/dispatch.js +102 -0
  10. package/build/cjs/index.js +58 -0
  11. package/build/cjs/routes/generated.js +1603 -0
  12. package/build/cjs/routes/index.js +111 -0
  13. package/build/cjs/routes/overlay.js +75 -0
  14. package/build/cjs/types.js +35 -0
  15. package/build/esm/corpus/drive.d.ts +33 -0
  16. package/build/esm/corpus/drive.d.ts.map +1 -0
  17. package/build/esm/corpus/drive.js +285 -0
  18. package/build/esm/corpus/runner.d.ts +27 -0
  19. package/build/esm/corpus/runner.d.ts.map +1 -0
  20. package/build/esm/corpus/runner.js +30 -0
  21. package/build/esm/drive/memory-store.d.ts +37 -0
  22. package/build/esm/drive/memory-store.d.ts.map +1 -0
  23. package/build/esm/drive/memory-store.js +122 -0
  24. package/build/esm/drive/paths.d.ts +49 -0
  25. package/build/esm/drive/paths.d.ts.map +1 -0
  26. package/build/esm/drive/paths.js +73 -0
  27. package/build/esm/drive/service.d.ts +42 -0
  28. package/build/esm/drive/service.d.ts.map +1 -0
  29. package/build/esm/drive/service.js +139 -0
  30. package/build/esm/host/dispatch.d.ts +24 -0
  31. package/build/esm/host/dispatch.d.ts.map +1 -0
  32. package/build/esm/host/dispatch.js +98 -0
  33. package/build/esm/index.d.ts +31 -0
  34. package/build/esm/index.d.ts.map +1 -0
  35. package/build/esm/index.js +24 -0
  36. package/build/esm/package.json +3 -0
  37. package/build/esm/routes/generated.d.ts +14 -0
  38. package/build/esm/routes/generated.d.ts.map +1 -0
  39. package/build/esm/routes/generated.js +1600 -0
  40. package/build/esm/routes/index.d.ts +18 -0
  41. package/build/esm/routes/index.d.ts.map +1 -0
  42. package/build/esm/routes/index.js +106 -0
  43. package/build/esm/routes/overlay.d.ts +40 -0
  44. package/build/esm/routes/overlay.d.ts.map +1 -0
  45. package/build/esm/routes/overlay.js +72 -0
  46. package/build/esm/types.d.ts +110 -0
  47. package/build/esm/types.d.ts.map +1 -0
  48. package/build/esm/types.js +30 -0
  49. package/package.json +55 -4
@@ -0,0 +1,18 @@
1
+ import type { Route } from '../types.js';
2
+ export declare const ROUTES: Route[];
3
+ /** The route with this `METHOD /path` identity, if the document has one. */
4
+ export declare const routeById: (id: string) => Route | undefined;
5
+ export interface RouteMatch {
6
+ route: Route;
7
+ /** Values captured from the path's `{placeholders}`. */
8
+ params: Record<string, string>;
9
+ }
10
+ /**
11
+ * The route a request names, or undefined when the API has no such operation.
12
+ *
13
+ * A host that gets undefined answers 404 "not part of the API"; a host that gets
14
+ * a route it cannot serve answers 501 naming the capability. Keeping those two
15
+ * apart is the difference between a client's bug and a host's limit.
16
+ */
17
+ export declare const findRoute: (method: string, path: string) => RouteMatch | undefined;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/routes/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAiB,MAAM,aAAa,CAAA;AAyCvD,eAAO,MAAM,MAAM,EAAE,KAAK,EAAgC,CAAA;AAW1D,4EAA4E;AAC5E,eAAO,MAAM,SAAS,GAAI,IAAI,MAAM,KAAG,KAAK,GAAG,SAAyB,CAAA;AAExE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAgCD;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GACpB,QAAQ,MAAM,EACd,MAAM,MAAM,KACX,UAAU,GAAG,SAuBf,CAAA"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The route table, and the one matcher both hosts dispatch with.
3
+ *
4
+ * `GENERATED_ROUTES` is the OpenAPI document, `OVERLAY` is the part a document
5
+ * cannot carry, and this file is where they meet. Importing it validates the
6
+ * join as a side effect: an overlay entry naming an operation the document does
7
+ * not have is a stale entry, and it throws here rather than being silently
8
+ * ignored - the failure mode that lets a "documented" behaviour rot.
9
+ */
10
+ import { GENERATED_ROUTES } from './generated.js';
11
+ import { OVERLAY, TAG_CAPABILITIES } from './overlay.js';
12
+ const merge = (generated) => {
13
+ const overlay = OVERLAY[generated.id] || {};
14
+ const documented = new Map(generated.responses.map((r) => [r.status, r.description]));
15
+ // The document states the statuses and the overlay only annotates the ones a
16
+ // client keys a body on. Naming a status the document does not state is a
17
+ // build failure: the alternative - listing it anyway - advertises a status
18
+ // nothing can observe without the parity test failing, which is a documented
19
+ // wish rather than a contract. A host that genuinely answers a status the
20
+ // document omits needs the document corrected in sasjs/server.
21
+ for (const status of Object.keys(overlay.responses || {}).map(Number)) {
22
+ if (!documented.has(status)) {
23
+ throw new Error(`src/routes/overlay.ts annotates status ${status} for ${generated.id}, which the OpenAPI document does not state. Correct the document in sasjs/server and re-vendor spec/swagger.yaml, or drop the annotation.`);
24
+ }
25
+ }
26
+ const responses = [...documented.keys()]
27
+ .sort((a, b) => a - b)
28
+ .map((status) => {
29
+ const body = overlay.responses ? overlay.responses[status] : undefined;
30
+ return {
31
+ status,
32
+ description: documented.get(status) || '',
33
+ ...(body === undefined ? {} : { body })
34
+ };
35
+ });
36
+ return {
37
+ ...generated,
38
+ responses,
39
+ capabilities: overlay.capabilities || TAG_CAPABILITIES[generated.tag] || [],
40
+ implemented: overlay.implemented === true
41
+ };
42
+ };
43
+ export const ROUTES = GENERATED_ROUTES.map(merge);
44
+ const known = new Set(ROUTES.map((route) => route.id));
45
+ for (const id of Object.keys(OVERLAY))
46
+ if (!known.has(id))
47
+ throw new Error(`src/routes/overlay.ts names an operation the OpenAPI document does not have: ${id}. Remove the entry or refresh spec/swagger.yaml.`);
48
+ const byId = new Map(ROUTES.map((route) => [route.id, route]));
49
+ /** The route with this `METHOD /path` identity, if the document has one. */
50
+ export const routeById = (id) => byId.get(id);
51
+ const escape = (segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ /**
53
+ * Compiles a documented path into a matcher. The table is static, so this runs
54
+ * once at import; `findRoute` then tests exact paths first (most of the API is
55
+ * static) and only falls through to the templates for `{uid}`-style paths.
56
+ */
57
+ const compiled = ROUTES.filter((route) => route.path.includes('{')).map((route) => {
58
+ const keys = [];
59
+ const pattern = route.path
60
+ .split('/')
61
+ .map((segment) => {
62
+ if (segment.startsWith('{') && segment.endsWith('}')) {
63
+ keys.push(segment.slice(1, -1));
64
+ return '([^/]+)';
65
+ }
66
+ return escape(segment);
67
+ })
68
+ .join('/');
69
+ return { route, keys, regex: new RegExp('^' + pattern + '/?$') };
70
+ });
71
+ const exact = new Map();
72
+ for (const route of ROUTES)
73
+ if (!route.path.includes('{'))
74
+ exact.set(route.method + ' ' + route.path, route);
75
+ /**
76
+ * The route a request names, or undefined when the API has no such operation.
77
+ *
78
+ * A host that gets undefined answers 404 "not part of the API"; a host that gets
79
+ * a route it cannot serve answers 501 naming the capability. Keeping those two
80
+ * apart is the difference between a client's bug and a host's limit.
81
+ */
82
+ export const findRoute = (method, path) => {
83
+ const verb = method.toUpperCase();
84
+ const clean = path.split('?')[0].replace(/\/+$/, '') || '/';
85
+ const hit = exact.get(verb + ' ' + clean);
86
+ if (hit)
87
+ return { route: hit, params: {} };
88
+ for (const candidate of compiled) {
89
+ if (candidate.route.method !== verb)
90
+ continue;
91
+ const match = candidate.regex.exec(clean);
92
+ if (!match)
93
+ continue;
94
+ const params = {};
95
+ candidate.keys.forEach((key, index) => {
96
+ try {
97
+ params[key] = decodeURIComponent(match[index + 1]);
98
+ }
99
+ catch {
100
+ params[key] = match[index + 1];
101
+ }
102
+ });
103
+ return { route: candidate.route, params };
104
+ }
105
+ return undefined;
106
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What the OpenAPI document cannot say.
3
+ *
4
+ * The generated table states the route's shape - method, path, parameters,
5
+ * documented responses. This file adds the three things a document has no room
6
+ * for, each of which a host or a test needs:
7
+ *
8
+ * - the body a response carries, because a client keys on `{"status":
9
+ * "success"}` and on the wording of a refusal, not on a status alone. A
10
+ * refusal's body is the message as a plain string: the drive routes answer
11
+ * `res.status(code).send(err.message)`, and sasjs/server's own suite asserts
12
+ * `res.text`. A host renders that in its own transport - a text body on the
13
+ * server, `text(...)` in the worker - and never wraps it in an envelope the
14
+ * server does not send;
15
+ * - which capability the route needs, so a host can answer 501 with a reason
16
+ * instead of 404 with none;
17
+ * - whether this package implements it yet, so "not done" is visible.
18
+ *
19
+ * The statuses come from the document, and an overlay body is attached to the
20
+ * status it belongs to: naming a status the document does not state is a build
21
+ * failure, not a way to describe a divergence. A divergence between the document
22
+ * and the code is a bug in one of them, corrected at source in `sasjs/server` and
23
+ * re-vendored here.
24
+ */
25
+ import type { Capability, HttpMethod } from '../types.js';
26
+ export interface RouteOverlay {
27
+ capabilities?: Capability[];
28
+ implemented?: boolean;
29
+ /**
30
+ * The body the operation sends with a status, where a client keys on it. The
31
+ * statuses come from the document; this only annotates them.
32
+ */
33
+ responses?: Record<number, unknown>;
34
+ }
35
+ /** The default capability for each tag, overridden per route where it differs. */
36
+ export declare const TAG_CAPABILITIES: Record<string, Capability[]>;
37
+ export declare const OVERLAY: Record<string, RouteOverlay>;
38
+ /** Every method the table may state, in the order a diff reads best. */
39
+ export declare const METHODS: HttpMethod[];
40
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEzD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAID,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAoDhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
@@ -0,0 +1,72 @@
1
+ /** The default capability for each tag, overridden per route where it differs. */
2
+ export const TAG_CAPABILITIES = {
3
+ Drive: ['storage'],
4
+ STP: ['runtime', 'storage'],
5
+ Code: ['runtime'],
6
+ Session: ['runtime'],
7
+ Info: [],
8
+ Admin: ['storage'],
9
+ Auth: ['identity'],
10
+ 'Auth Config': ['identity'],
11
+ User: ['identity'],
12
+ Group: ['identity'],
13
+ Permission: ['identity'],
14
+ Client: ['identity'],
15
+ Web: ['identity']
16
+ };
17
+ const success = { status: 'success' };
18
+ export const OVERLAY = {
19
+ // the route inventory reports what the CALLER may use, so it needs an identity
20
+ // even though the rest of the Info tag needs nothing
21
+ 'GET /SASjsApi/info/authorizedRoutes': { capabilities: ['identity'] },
22
+ 'GET /SASjsApi/info/myAuthorizedRoutes': { capabilities: ['identity'] },
23
+ // the body is the file itself, so there is nothing to annotate
24
+ 'GET /SASjsApi/drive/file': { implemented: true },
25
+ 'POST /SASjsApi/drive/file': {
26
+ implemented: true,
27
+ responses: { 200: success, 409: 'File already exists.' }
28
+ },
29
+ 'PATCH /SASjsApi/drive/file': {
30
+ implemented: true,
31
+ responses: { 200: success, 404: `File doesn't exist.` }
32
+ },
33
+ 'DELETE /SASjsApi/drive/file': {
34
+ implemented: true,
35
+ responses: { 200: success, 404: `File doesn't exist.` }
36
+ },
37
+ 'GET /SASjsApi/drive/folder': {
38
+ implemented: true,
39
+ responses: { 200: { files: [], folders: [] } }
40
+ },
41
+ 'POST /SASjsApi/drive/folder': {
42
+ implemented: true,
43
+ responses: { 200: success, 409: 'Folder already exists.' }
44
+ },
45
+ 'DELETE /SASjsApi/drive/folder': {
46
+ implemented: true,
47
+ responses: { 200: success, 404: `Folder doesn't exist.` }
48
+ },
49
+ 'POST /SASjsApi/drive/rename': {
50
+ implemented: true,
51
+ responses: {
52
+ 200: success,
53
+ 409: 'File with new name already exists.',
54
+ 404: 'No file/folder found for provided path.'
55
+ }
56
+ },
57
+ 'GET /SASjsApi/drive/filetree': {
58
+ implemented: true,
59
+ responses: { 200: { status: 'success', tree: {} } }
60
+ },
61
+ // The two deploy routes write a whole tree in one request. They are the only
62
+ // ones where a caller can exhaust the host's storage, so they carry a cap -
63
+ // see the plan in sasjs/cli#1550 - and they are not implemented yet.
64
+ 'POST /SASjsApi/drive/deploy': {
65
+ implemented: false
66
+ },
67
+ 'POST /SASjsApi/drive/deploy/upload': {
68
+ implemented: false
69
+ }
70
+ };
71
+ /** Every method the table may state, in the order a diff reads best. */
72
+ export const METHODS = ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'];
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The vocabulary the two hosts share.
3
+ *
4
+ * Nothing here imports a runtime: the package is consumed by an Express server
5
+ * in node and by a service worker in a browser, so the types are all this file
6
+ * may contain. That is also why the build compiles `src` with `types: []` - an
7
+ * accidental `Buffer` or `process` reference fails the build rather than the
8
+ * worker at run time.
9
+ */
10
+ /**
11
+ * A refusal the API answers with: the status a client keys on, and the message
12
+ * it prints. Both hosts throw this and both render it the same way, so the
13
+ * corpus can assert on the pair.
14
+ */
15
+ export declare class ApiError extends Error {
16
+ readonly status: number;
17
+ /** Told apart by marker rather than `instanceof` - see `isApiError`. */
18
+ readonly apiError = true;
19
+ constructor(status: number, message: string);
20
+ }
21
+ /**
22
+ * `instanceof` survives here (the package targets ES2020, not ES5), but a
23
+ * consumer that downlevels it - the CLI does - loses the prototype chain, so the
24
+ * marker is what the checks use. Asserting both in a spec is what keeps that
25
+ * honest.
26
+ */
27
+ export declare const isApiError: (value: unknown) => value is ApiError;
28
+ /**
29
+ * What a route needs from a host in order to answer it.
30
+ *
31
+ * `runtime` is a program runtime - the host decides which languages it holds
32
+ * (the browser build runs JavaScript, a server runs JavaScript and SAS) and
33
+ * answers 501 when the program it was given is in a language it has no runtime
34
+ * for. `sas` is a SAS engine specifically, for a route that cannot be answered
35
+ * without one.
36
+ */
37
+ export type Capability = 'runtime' | 'storage' | 'identity' | 'sas' | 'node';
38
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
39
+ export type ParamLocation = 'query' | 'path' | 'body' | 'formData' | 'header';
40
+ export interface RouteParam {
41
+ name: string;
42
+ in: ParamLocation;
43
+ required: boolean;
44
+ /** JSON-schema type as the OpenAPI document words it. */
45
+ type: string;
46
+ enum?: string[];
47
+ }
48
+ export interface RouteResponse {
49
+ status: number;
50
+ description: string;
51
+ /** The body the server actually sends, where the corpus pins one. */
52
+ body?: unknown;
53
+ }
54
+ /** An operation exactly as the OpenAPI document states it. */
55
+ export interface GeneratedRoute {
56
+ id: string;
57
+ method: HttpMethod;
58
+ path: string;
59
+ tag: string;
60
+ summary: string;
61
+ params: RouteParam[];
62
+ requestBodies: string[];
63
+ responses: Array<{
64
+ status: number;
65
+ description: string;
66
+ }>;
67
+ }
68
+ export interface Route extends GeneratedRoute {
69
+ responses: RouteResponse[];
70
+ capabilities: Capability[];
71
+ /**
72
+ * Whether this package carries a handler for the operation. A host may still
73
+ * refuse it for a missing capability - that is a 501 naming the capability,
74
+ * not an absence nobody can see.
75
+ */
76
+ implemented: boolean;
77
+ }
78
+ /**
79
+ * The drive, as the services see it.
80
+ *
81
+ * Every path is drive-relative and already normalised by `normaliseDrivePath`:
82
+ * it starts with `/`, never contains `.` or `..`, and cannot leave the drive.
83
+ * The store owns the translation to wherever the bytes actually live - a folder
84
+ * under node, a Map in the browser - and owns the representation it uses to do
85
+ * it (text vs bytes).
86
+ */
87
+ export interface DriveStore {
88
+ read(path: string): Promise<string | Uint8Array>;
89
+ write(path: string, contents: string | Uint8Array): Promise<void>;
90
+ exists(path: string): Promise<boolean>;
91
+ stat(path: string): Promise<{
92
+ directory: boolean;
93
+ size: number;
94
+ }>;
95
+ list(path: string): Promise<{
96
+ files: string[];
97
+ folders: string[];
98
+ }>;
99
+ mkdirp(path: string): Promise<void>;
100
+ /** Removes a file, or a folder with everything under it. */
101
+ remove(path: string): Promise<void>;
102
+ move(from: string, to: string): Promise<void>;
103
+ }
104
+ export interface TreeNode {
105
+ name: string;
106
+ relativePath: string;
107
+ isFolder: boolean;
108
+ children: TreeNode[];
109
+ }
110
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The vocabulary the two hosts share.
3
+ *
4
+ * Nothing here imports a runtime: the package is consumed by an Express server
5
+ * in node and by a service worker in a browser, so the types are all this file
6
+ * may contain. That is also why the build compiles `src` with `types: []` - an
7
+ * accidental `Buffer` or `process` reference fails the build rather than the
8
+ * worker at run time.
9
+ */
10
+ /**
11
+ * A refusal the API answers with: the status a client keys on, and the message
12
+ * it prints. Both hosts throw this and both render it the same way, so the
13
+ * corpus can assert on the pair.
14
+ */
15
+ export class ApiError extends Error {
16
+ constructor(status, message) {
17
+ super(message);
18
+ this.status = status;
19
+ /** Told apart by marker rather than `instanceof` - see `isApiError`. */
20
+ this.apiError = true;
21
+ this.name = 'ApiError';
22
+ }
23
+ }
24
+ /**
25
+ * `instanceof` survives here (the package targets ES2020, not ES5), but a
26
+ * consumer that downlevels it - the CLI does - loses the prototype chain, so the
27
+ * marker is what the checks use. Asserting both in a spec is what keeps that
28
+ * honest.
29
+ */
30
+ export const isApiError = (value) => !!value && value.apiError === true;
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@sasjs/api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "The SASjs Server API, described once - a generated route table, the services behind it, and the conformance corpus both hosts run",
5
+ "license": "MIT",
6
+ "author": "SASjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sasjs/api.git"
10
+ },
11
+ "homepage": "https://github.com/sasjs/api#readme",
12
+ "main": "build/cjs/index.js",
13
+ "module": "build/esm/index.js",
14
+ "types": "build/esm/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./build/esm/index.d.ts",
18
+ "import": "./build/esm/index.js",
19
+ "require": "./build/cjs/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "build",
24
+ "README.md",
25
+ "LICENCE.md"
26
+ ],
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": "^22.14.0 || >=24.10.0"
30
+ },
31
+ "scripts": {
32
+ "build": "rimraf build && tsc -p tsconfig.build.cjs.json && tsc -p tsconfig.build.esm.json && node scripts/postbuild.mjs && node scripts/build-browser.mjs",
33
+ "test": "jest --coverage",
34
+ "test:watch": "jest --watch",
35
+ "lint": "prettier --check \"{src,spec,scripts}/**/*.{ts,mjs,md}\" \"*.{json,md}\"",
36
+ "lint:fix": "prettier --write \"{src,spec,scripts}/**/*.{ts,mjs,md}\" \"*.{json,md}\"",
37
+ "sync:routes": "node scripts/sync-routes.mjs",
38
+ "check:routes": "node scripts/sync-routes.mjs --check"
39
+ },
40
+ "devDependencies": {
41
+ "@semantic-release/commit-analyzer": "13.0.1",
42
+ "@semantic-release/exec": "7.1.0",
43
+ "@semantic-release/github": "12.0.10",
44
+ "@semantic-release/npm": "13.2.0",
45
+ "@semantic-release/release-notes-generator": "14.1.1",
46
+ "@types/jest": "29.5.14",
47
+ "@types/node": "22.20.5",
48
+ "esbuild": "0.28.2",
49
+ "jest": "29.7.0",
50
+ "prettier": "3.9.10",
51
+ "rimraf": "6.1.4",
52
+ "semantic-release": "25.0.9",
53
+ "ts-jest": "29.4.15",
54
+ "typescript": "5.9.3",
55
+ "yaml": "2.9.1"
56
+ }
57
+ }