@oxyhq/core 19.1.1 → 19.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "19.1.1",
3
+ "version": "19.1.2",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -121,6 +121,7 @@
121
121
  "@types/elliptic": "^6.4.18",
122
122
  "buffer": "^6.0.3",
123
123
  "elliptic": "^6.6.1",
124
+ "helmet": "^8.0.0",
124
125
  "invariant": "^2.2.4",
125
126
  "jwt-decode": "^4.0.0",
126
127
  "socket.io-client": "^4.8.1",
@@ -132,8 +133,7 @@
132
133
  "expo-crypto": "*",
133
134
  "expo-secure-store": "*",
134
135
  "express": "^4.0.0",
135
- "express-rate-limit": "^8.0.0",
136
- "helmet": "^8.0.0"
136
+ "express-rate-limit": "^8.0.0"
137
137
  },
138
138
  "peerDependenciesMeta": {
139
139
  "@react-native-async-storage/async-storage": {
@@ -147,9 +147,6 @@
147
147
  },
148
148
  "express": {
149
149
  "optional": true
150
- },
151
- "helmet": {
152
- "optional": true
153
150
  }
154
151
  },
155
152
  "devDependencies": {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Every package `@oxyhq/core/server` VALUE-imports must be a real dependency.
3
+ *
4
+ * ## The failure this exists for, which happened
5
+ *
6
+ * `helmet` was declared an OPTIONAL peer dependency. `src/server/securityHeaders.ts`
7
+ * imports it at the top level, and the server barrel re-exports that module — so
8
+ * importing *anything* from `@oxyhq/core/server` loads helmet, and a consumer
9
+ * that believed the "optional" marking did not install it.
10
+ *
11
+ * The result was a backend that could not boot: `Cannot find package 'helmet'`.
12
+ * `tsc` passed clean the whole time, because types resolve from a peer whether
13
+ * or not the runtime can find the module. It was found by a test suite failing
14
+ * to resolve 29 of 63 files while every typecheck was green — which is a very
15
+ * expensive way to learn it.
16
+ *
17
+ * ## Why value imports and not all imports
18
+ *
19
+ * `express` is imported by eight files here and every one of them is
20
+ * `import type`. Those vanish at build time, so an optional peer is a truthful
21
+ * declaration for it: a consumer of the client entry never needs express
22
+ * installed. The distinction this test draws is exactly the one that decides
23
+ * whether a missing package is a warning or a crash.
24
+ */
25
+
26
+ import { readFileSync, readdirSync } from 'node:fs';
27
+ import path from 'node:path';
28
+
29
+ const packageRoot = path.resolve(__dirname, '../..');
30
+ const serverDir = path.join(packageRoot, 'src', 'server');
31
+
32
+ const manifest = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')) as {
33
+ dependencies?: Record<string, string>;
34
+ peerDependencies?: Record<string, string>;
35
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
36
+ };
37
+
38
+ const dependencies = new Set(Object.keys(manifest.dependencies ?? {}));
39
+ const requiredPeers = new Set(
40
+ Object.keys(manifest.peerDependencies ?? {}).filter(
41
+ (name) => manifest.peerDependenciesMeta?.[name]?.optional !== true
42
+ )
43
+ );
44
+
45
+ /**
46
+ * Bare specifiers imported for their VALUE — `import x from 'y'`,
47
+ * `import { a } from 'y'`, `import 'y'` — but never `import type`.
48
+ */
49
+ function valueImports(source: string): string[] {
50
+ const found: string[] = [];
51
+ // Strip comments first: prose in a doc comment can otherwise read as code.
52
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1 ');
53
+ for (const match of code.matchAll(/import\s+(?!type\b)([^;']*?)from\s*'([^']+)'/g)) {
54
+ // `import { type A, type B } from 'x'` is still type-only in effect.
55
+ const clause = match[1];
56
+ const specifier = match[2];
57
+ if (specifier.startsWith('.') || specifier.startsWith('node:')) continue;
58
+ const names = clause.replace(/[{}]/g, '').split(',').map((n) => n.trim()).filter(Boolean);
59
+ if (names.length > 0 && names.every((n) => n.startsWith('type '))) continue;
60
+ found.push(specifier);
61
+ }
62
+ for (const match of code.matchAll(/import\s*'([^']+)'/g)) {
63
+ if (!match[1].startsWith('.') && !match[1].startsWith('node:')) found.push(match[1]);
64
+ }
65
+ return found;
66
+ }
67
+
68
+ function packageNameOf(specifier: string): string {
69
+ const parts = specifier.split('/');
70
+ return specifier.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
71
+ }
72
+
73
+ const files = readdirSync(serverDir).filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts'));
74
+
75
+ describe('@oxyhq/core/server value imports are installable', () => {
76
+ it('reads a non-trivial number of server modules', () => {
77
+ // Vacuity floor: a traversal that found nothing would satisfy every
78
+ // assertion below by having nothing to check.
79
+ expect(files.length).toBeGreaterThan(5);
80
+ });
81
+
82
+ it('declares every value-imported package as a dependency or a REQUIRED peer', () => {
83
+ const undeclared: string[] = [];
84
+ for (const file of files) {
85
+ const source = readFileSync(path.join(serverDir, file), 'utf8');
86
+ for (const specifier of valueImports(source)) {
87
+ const name = packageNameOf(specifier);
88
+ if (dependencies.has(name) || requiredPeers.has(name)) continue;
89
+ undeclared.push(`${file} → ${name}`);
90
+ }
91
+ }
92
+
93
+ // An OPTIONAL peer is not enough here and that is the whole point: the
94
+ // module is loaded the moment anything is imported from the server barrel,
95
+ // so "optional" describes a package the consumer cannot actually omit.
96
+ expect(undeclared.sort()).toEqual([]);
97
+ });
98
+
99
+ it('keeps helmet installable rather than optional', () => {
100
+ // The specific regression, asserted by name so a failure says what broke
101
+ // rather than only showing it inside a list.
102
+ expect(dependencies.has('helmet')).toBe(true);
103
+ expect(manifest.peerDependenciesMeta?.helmet?.optional).toBeUndefined();
104
+ });
105
+
106
+ it('leaves express as an optional peer, because it is type-only here', () => {
107
+ // The counter-example that stops this test from being read as "declare
108
+ // everything": type imports vanish at build time, so an optional peer is a
109
+ // truthful declaration for express and making it required would nag every
110
+ // React Native consumer about a package they never load.
111
+ expect(manifest.peerDependenciesMeta?.express?.optional).toBe(true);
112
+ });
113
+ });