@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.
|
|
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
|
+
});
|