@endo/compartment-mapper 1.6.2 → 2.0.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 (94) hide show
  1. package/package.json +12 -16
  2. package/src/archive-lite.d.ts +7 -7
  3. package/src/archive-lite.d.ts.map +1 -1
  4. package/src/archive-lite.js +78 -27
  5. package/src/archive.d.ts.map +1 -1
  6. package/src/archive.js +7 -0
  7. package/src/bundle-lite.d.ts +3 -3
  8. package/src/bundle-lite.d.ts.map +1 -1
  9. package/src/bundle-lite.js +19 -24
  10. package/src/bundle.d.ts +3 -3
  11. package/src/bundle.d.ts.map +1 -1
  12. package/src/bundle.js +19 -24
  13. package/src/capture-lite.d.ts +2 -2
  14. package/src/capture-lite.d.ts.map +1 -1
  15. package/src/capture-lite.js +217 -25
  16. package/src/compartment-map.d.ts +9 -2
  17. package/src/compartment-map.d.ts.map +1 -1
  18. package/src/compartment-map.js +737 -254
  19. package/src/digest.d.ts +22 -2
  20. package/src/digest.d.ts.map +1 -1
  21. package/src/digest.js +179 -56
  22. package/src/generic-graph.d.ts +84 -0
  23. package/src/generic-graph.d.ts.map +1 -0
  24. package/src/generic-graph.js +356 -0
  25. package/src/guards.d.ts +18 -0
  26. package/src/guards.d.ts.map +1 -0
  27. package/src/guards.js +109 -0
  28. package/src/hooks.md +124 -0
  29. package/src/import-archive-lite.d.ts.map +1 -1
  30. package/src/import-archive-lite.js +15 -11
  31. package/src/import-archive.d.ts +5 -19
  32. package/src/import-archive.d.ts.map +1 -1
  33. package/src/import-archive.js +7 -27
  34. package/src/import-hook.d.ts +4 -3
  35. package/src/import-hook.d.ts.map +1 -1
  36. package/src/import-hook.js +156 -71
  37. package/src/import-lite.d.ts +6 -6
  38. package/src/import-lite.d.ts.map +1 -1
  39. package/src/import-lite.js +8 -5
  40. package/src/import.d.ts +3 -3
  41. package/src/import.d.ts.map +1 -1
  42. package/src/import.js +16 -6
  43. package/src/infer-exports.d.ts.map +1 -1
  44. package/src/infer-exports.js +16 -6
  45. package/src/json.d.ts +1 -1
  46. package/src/json.d.ts.map +1 -1
  47. package/src/json.js +10 -3
  48. package/src/link.d.ts +4 -3
  49. package/src/link.d.ts.map +1 -1
  50. package/src/link.js +70 -58
  51. package/src/node-modules.d.ts +5 -3
  52. package/src/node-modules.d.ts.map +1 -1
  53. package/src/node-modules.js +648 -245
  54. package/src/node-powers.d.ts +6 -5
  55. package/src/node-powers.d.ts.map +1 -1
  56. package/src/node-powers.js +11 -8
  57. package/src/parse-cjs-shared-export-wrapper.d.ts.map +1 -1
  58. package/src/parse-cjs-shared-export-wrapper.js +3 -1
  59. package/src/policy-format.d.ts +22 -5
  60. package/src/policy-format.d.ts.map +1 -1
  61. package/src/policy-format.js +342 -108
  62. package/src/policy.d.ts +13 -28
  63. package/src/policy.d.ts.map +1 -1
  64. package/src/policy.js +161 -106
  65. package/src/types/canonical-name.d.ts +97 -0
  66. package/src/types/canonical-name.d.ts.map +1 -0
  67. package/src/types/canonical-name.ts +151 -0
  68. package/src/types/compartment-map-schema.d.ts +114 -35
  69. package/src/types/compartment-map-schema.d.ts.map +1 -1
  70. package/src/types/compartment-map-schema.ts +202 -37
  71. package/src/types/external.d.ts +173 -29
  72. package/src/types/external.d.ts.map +1 -1
  73. package/src/types/external.ts +221 -27
  74. package/src/types/generic-graph.d.ts +17 -0
  75. package/src/types/generic-graph.d.ts.map +1 -0
  76. package/src/types/generic-graph.ts +17 -0
  77. package/src/types/internal.d.ts +24 -42
  78. package/src/types/internal.d.ts.map +1 -1
  79. package/src/types/internal.ts +52 -50
  80. package/src/types/node-modules.d.ts +101 -17
  81. package/src/types/node-modules.d.ts.map +1 -1
  82. package/src/types/node-modules.ts +142 -17
  83. package/src/types/policy-schema.d.ts +26 -11
  84. package/src/types/policy-schema.d.ts.map +1 -1
  85. package/src/types/policy-schema.ts +29 -16
  86. package/src/types/policy.d.ts +6 -2
  87. package/src/types/policy.d.ts.map +1 -1
  88. package/src/types/policy.ts +7 -2
  89. package/src/types/powers.d.ts +38 -11
  90. package/src/types/powers.d.ts.map +1 -1
  91. package/src/types/powers.ts +50 -17
  92. package/src/types/typescript.d.ts +28 -0
  93. package/src/types/typescript.d.ts.map +1 -1
  94. package/src/types/typescript.ts +37 -1
package/src/policy.js CHANGED
@@ -4,39 +4,46 @@
4
4
  * @module
5
5
  */
6
6
 
7
- // @ts-check
8
-
9
7
  /**
10
8
  * @import {
11
9
  * Policy,
12
10
  * PackagePolicy,
13
11
  * AttenuationDefinition,
14
- * PackageNamingKit,
15
12
  * DeferredAttenuatorsProvider,
16
13
  * CompartmentDescriptor,
17
14
  * Attenuator,
18
15
  * SomePolicy,
16
+ * PolicyEnforcementField,
19
17
  * SomePackagePolicy,
18
+ * CompartmentDescriptorWithPolicy,
19
+ * ModuleConfiguration,
20
+ * CanonicalName,
20
21
  * } from './types.js'
22
+ * @import {ThirdPartyStaticModuleInterface} from 'ses'
21
23
  */
22
24
 
23
25
  import {
26
+ ATTENUATORS_COMPARTMENT,
27
+ ENTRY_COMPARTMENT,
24
28
  getAttenuatorFromDefinition,
25
29
  isAllowingEverything,
26
30
  isAttenuationDefinition,
27
31
  policyLookupHelper,
32
+ WILDCARD_POLICY_VALUE,
28
33
  } from './policy-format.js';
29
34
 
30
- const { create, entries, values, assign, freeze, getOwnPropertyDescriptors } =
31
- Object;
35
+ const {
36
+ keys,
37
+ create,
38
+ entries,
39
+ values,
40
+ assign,
41
+ freeze,
42
+ getOwnPropertyDescriptors,
43
+ } = Object;
32
44
  const { ownKeys } = Reflect;
33
45
  const q = JSON.stringify;
34
46
 
35
- /**
36
- * Const string to identify the internal attenuators compartment
37
- */
38
- export const ATTENUATORS_COMPARTMENT = '<ATTENUATORS>';
39
-
40
47
  /**
41
48
  * Copies properties (optionally limited to a specific list) from one object to another.
42
49
  * @template {Record<PropertyKey, any>} T
@@ -107,80 +114,55 @@ export const detectAttenuators = policy => {
107
114
  return attenuatorsCache.get(policy);
108
115
  };
109
116
 
110
- /**
111
- * Generates a string identifying a package for policy lookup purposes.
112
- *
113
- * @param {PackageNamingKit} namingKit
114
- * @returns {string}
115
- */
116
- const generateCanonicalName = ({ isEntry = false, name, path }) => {
117
- if (isEntry) {
118
- throw Error('Entry module cannot be identified with a canonicalName');
119
- }
120
- if (name === ATTENUATORS_COMPARTMENT) {
121
- return ATTENUATORS_COMPARTMENT;
122
- }
123
- return path.join('>');
124
- };
125
-
126
117
  /**
127
118
  * Verifies if a module identified by `namingKit` can be a dependency of a package per `packagePolicy`.
128
119
  * `packagePolicy` is required, when policy is not set, skipping needs to be handled by the caller.
129
120
  *
130
- * @param {PackageNamingKit} namingKit
121
+ * @param {CanonicalName} canonicalName
131
122
  * @param {PackagePolicy} packagePolicy
132
123
  * @returns {boolean}
133
124
  */
134
- export const dependencyAllowedByPolicy = (namingKit, packagePolicy) => {
135
- if (namingKit.isEntry) {
136
- // dependency on entry compartment should never be allowed
137
- return false;
138
- }
139
- const canonicalName = generateCanonicalName(namingKit);
125
+ export const dependencyAllowedByPolicy = (canonicalName, packagePolicy) => {
140
126
  return !!policyLookupHelper(packagePolicy, 'packages', canonicalName);
141
127
  };
142
128
 
143
129
  /**
144
- * Returns the policy applicable to the canonicalName of the package
145
- *
146
- * @overload
147
- * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these
148
- * @param {SomePolicy} policy - user supplied policy
149
- * @returns {SomePackagePolicy} packagePolicy if policy was specified
150
- */
151
-
152
- /**
153
- * Returns `undefined`
154
- *
155
- * @overload
156
- * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these
157
- * @param {SomePolicy} [policy] - user supplied policy
158
- * @returns {SomePackagePolicy|undefined} packagePolicy if policy was specified
159
- */
160
-
161
- /**
162
- * Returns the policy applicable to the canonicalName of the package
130
+ * Generates the {@link SomePackagePolicy} value to be used in
131
+ * {@link CompartmentDescriptor.policy}
163
132
  *
164
- * @param {PackageNamingKit} namingKit - a key in the policy resources spec is derived from these
165
- * @param {SomePolicy} [policy] - user supplied policy
133
+ * @param {CanonicalName | typeof ATTENUATORS_COMPARTMENT | typeof ENTRY_COMPARTMENT} label
134
+ * @param {object} [options] Options
135
+ * @param {SomePolicy} [options.policy] User-supplied policy
136
+ * @returns {SomePackagePolicy|undefined} Package policy from `policy` or empty
137
+ * object; returns `params.packagePolicy` if provided. If entry compartment,
138
+ * returns the `entry` property of the policy verbatim.
166
139
  */
167
- export const getPolicyForPackage = (namingKit, policy) => {
168
- if (!policy) {
169
- return undefined;
170
- }
171
- if (namingKit.isEntry) {
172
- return policy.entry;
173
- }
174
- const canonicalName = generateCanonicalName(namingKit);
175
- if (canonicalName === ATTENUATORS_COMPARTMENT) {
176
- return { defaultAttenuator: policy.defaultAttenuator, packages: 'any' };
140
+ export const makePackagePolicy = (label, { policy } = {}) => {
141
+ /** @type {SomePackagePolicy|undefined} */
142
+ let packagePolicy;
143
+ if (!label) {
144
+ throw new TypeError(
145
+ `Invalid arguments: label must be a non-empty string; got ${q(label)}`,
146
+ );
177
147
  }
178
- if (policy.resources && policy.resources[canonicalName] !== undefined) {
179
- return policy.resources[canonicalName];
180
- } else {
181
- // Allow skipping policy entries for packages with no powers.
182
- return create(null);
148
+ if (policy) {
149
+ if (label === ATTENUATORS_COMPARTMENT) {
150
+ packagePolicy = {
151
+ defaultAttenuator: policy.defaultAttenuator,
152
+ packages: WILDCARD_POLICY_VALUE,
153
+ };
154
+ } else if (label === ENTRY_COMPARTMENT) {
155
+ packagePolicy = policy.entry;
156
+ // If policy.entry is `undefined`, we return `undefined` which is
157
+ // equivalent to "allow everything".
158
+ return packagePolicy;
159
+ } else if (label) {
160
+ packagePolicy = policy.resources?.[label];
161
+ }
162
+ // An empty object for a package policy is equivalent to "allow nothing"
163
+ return packagePolicy ?? create(null);
183
164
  }
165
+ return undefined;
184
166
  };
185
167
 
186
168
  /**
@@ -249,6 +231,11 @@ export const makeDeferredAttenuatorsProvider = (
249
231
  throw Error(`No attenuators specified in policy`);
250
232
  };
251
233
  } else {
234
+ if (!compartmentDescriptors[ATTENUATORS_COMPARTMENT].policy) {
235
+ throw Error(
236
+ `${q(ATTENUATORS_COMPARTMENT)} is missing the required policy; this is likely a bug`,
237
+ );
238
+ }
252
239
  defaultAttenuator =
253
240
  compartmentDescriptors[ATTENUATORS_COMPARTMENT].policy.defaultAttenuator;
254
241
 
@@ -321,7 +308,7 @@ async function attenuateGlobalThis({
321
308
  *
322
309
  * @param {object} globalThis
323
310
  * @param {object} globals
324
- * @param {PackagePolicy} packagePolicy
311
+ * @param {PackagePolicy|undefined} packagePolicy
325
312
  * @param {DeferredAttenuatorsProvider} attenuators
326
313
  * @param {Array<Promise>} pendingJobs
327
314
  * @param {string} name
@@ -376,18 +363,46 @@ export const attenuateGlobals = (
376
363
  };
377
364
 
378
365
  /**
379
- * @param {string} [errorHint]
366
+ * Generates a helpful error message for a policy enforcement failure
367
+ *
368
+ * @param {string} specifier
369
+ * @param {CompartmentDescriptorWithPolicy} referrerCompartmentDescriptor
370
+ * @param {object} options
371
+ * @param {string} [options.resourceCanonicalName]
372
+ * @param {string} [options.errorHint]
373
+ * @param {PolicyEnforcementField} [options.policyField]
380
374
  * @returns {string}
381
375
  */
382
- const diagnoseModulePolicy = errorHint => {
383
- if (!errorHint) {
384
- return '';
376
+ const policyEnforcementFailureMessage = (
377
+ specifier,
378
+ { label, policy },
379
+ { resourceCanonicalName, errorHint, policyField = 'packages' } = {},
380
+ ) => {
381
+ let message = `Importing ${q(specifier)}`;
382
+ if (resourceCanonicalName) {
383
+ message += ` in resource ${q(resourceCanonicalName)}`;
385
384
  }
386
- return ` (info: ${errorHint})`;
385
+ message += ` in ${q(label)} was not allowed by`;
386
+ if (keys(policy[policyField] ?? {}).length > 0) {
387
+ message += ` ${q(policyField)} policy: ${q(policy[policyField])}`;
388
+ } else {
389
+ message += ` empty ${q(policyField)} policy`;
390
+ }
391
+ if (errorHint) {
392
+ message += ` (info: ${errorHint})`;
393
+ }
394
+ return message;
387
395
  };
388
396
 
389
397
  /**
390
- * Options for {@link enforceModulePolicy}
398
+ * @template {ModuleConfiguration} T
399
+ * @param {CompartmentDescriptor<T>} compartmentDescriptor
400
+ * @returns {compartmentDescriptor is CompartmentDescriptorWithPolicy<T>}
401
+ */
402
+ const hasPolicy = compartmentDescriptor => !!compartmentDescriptor.policy;
403
+
404
+ /**
405
+ * Options for {@link enforcePolicyByModule}
391
406
  * @typedef EnforceModulePolicyOptions
392
407
  * @property {boolean} [exit] - Whether it is an exit module
393
408
  * @property {string} [errorHint] - Error hint message
@@ -400,34 +415,69 @@ const diagnoseModulePolicy = errorHint => {
400
415
  * @param {CompartmentDescriptor} compartmentDescriptor
401
416
  * @param {EnforceModulePolicyOptions} [options]
402
417
  */
403
- export const enforceModulePolicy = (
418
+ export const enforcePolicyByModule = (
404
419
  specifier,
405
420
  compartmentDescriptor,
406
421
  { exit, errorHint } = {},
407
422
  ) => {
408
- const { policy, modules, label } = compartmentDescriptor;
409
- if (!policy) {
423
+ if (!hasPolicy(compartmentDescriptor)) {
424
+ // No policy, no enforcement
410
425
  return;
411
426
  }
427
+ const { policy, modules } = compartmentDescriptor;
412
428
 
413
429
  if (!exit) {
414
430
  if (!modules[specifier]) {
415
431
  throw Error(
416
- `Importing ${q(specifier)} in ${q(
417
- label,
418
- )} was not allowed by packages policy ${q(
419
- policy.packages,
420
- )}${diagnoseModulePolicy(errorHint)}`,
432
+ policyEnforcementFailureMessage(specifier, compartmentDescriptor, {
433
+ errorHint,
434
+ }),
421
435
  );
422
436
  }
437
+
423
438
  return;
424
439
  }
425
440
 
426
441
  if (!policyLookupHelper(policy, 'builtins', specifier)) {
427
442
  throw Error(
428
- `Importing ${q(specifier)} was not allowed by policy builtins:${q(
429
- policy.builtins,
430
- )}${diagnoseModulePolicy(errorHint)}`,
443
+ policyEnforcementFailureMessage(specifier, compartmentDescriptor, {
444
+ errorHint,
445
+ policyField: 'builtins',
446
+ }),
447
+ );
448
+ }
449
+ };
450
+
451
+ /**
452
+ * Throws if importing `compartmentDescriptor` from `referrerCompartmentDescriptor` is not allowed per package policy
453
+ *
454
+ * @param {CompartmentDescriptor} compartmentDescriptor
455
+ * @param {CompartmentDescriptor} referrerCompartmentDescriptor
456
+ * @param {EnforceModulePolicyOptions} [options]
457
+ */
458
+ export const enforcePackagePolicyByCanonicalName = (
459
+ compartmentDescriptor,
460
+ referrerCompartmentDescriptor,
461
+ { errorHint } = {},
462
+ ) => {
463
+ if (!hasPolicy(referrerCompartmentDescriptor)) {
464
+ throw new Error(
465
+ `Cannot enforce policy via ${q(referrerCompartmentDescriptor.label)}: no package policy defined`,
466
+ );
467
+ }
468
+ const { policy: referrerPolicy } = referrerCompartmentDescriptor;
469
+ const { label: resourceCanonicalName } = compartmentDescriptor;
470
+
471
+ if (!policyLookupHelper(referrerPolicy, 'packages', resourceCanonicalName)) {
472
+ throw new Error(
473
+ policyEnforcementFailureMessage(
474
+ resourceCanonicalName,
475
+ referrerCompartmentDescriptor,
476
+ {
477
+ errorHint,
478
+ resourceCanonicalName,
479
+ },
480
+ ),
431
481
  );
432
482
  }
433
483
  };
@@ -437,8 +487,8 @@ export const enforceModulePolicy = (
437
487
  * @param {object} options
438
488
  * @param {DeferredAttenuatorsProvider} options.attenuators
439
489
  * @param {AttenuationDefinition} options.attenuationDefinition
440
- * @param {import('ses').ThirdPartyStaticModuleInterface} options.originalModuleRecord
441
- * @returns {Promise<import('ses').ThirdPartyStaticModuleInterface>}
490
+ * @param {ThirdPartyStaticModuleInterface} options.originalModuleRecord
491
+ * @returns {Promise<ThirdPartyStaticModuleInterface>}
442
492
  */
443
493
  async function attenuateModule({
444
494
  attenuators,
@@ -454,29 +504,31 @@ async function attenuateModule({
454
504
  // An async attenuator maker could be introduced here to return a synchronous attenuator.
455
505
  // For async attenuators see PR https://github.com/endojs/endo/pull/1535
456
506
 
457
- return freeze({
458
- imports: originalModuleRecord.imports,
459
- // It seems ok to declare the exports but then let the attenuator trim the values.
460
- // Seems ok for attenuation to leave them undefined - accessing them is malicious behavior.
461
- exports: originalModuleRecord.exports,
462
- execute: (moduleExports, compartment, resolvedImports) => {
463
- const ns = {};
464
- originalModuleRecord.execute(ns, compartment, resolvedImports);
465
- const attenuated = attenuate(ns);
466
- moduleExports.default = attenuated;
467
- assign(moduleExports, attenuated);
468
- },
469
- });
507
+ return freeze(
508
+ /** @type {ThirdPartyStaticModuleInterface} */ ({
509
+ imports: originalModuleRecord.imports,
510
+ // It seems ok to declare the exports but then let the attenuator trim the values.
511
+ // Seems ok for attenuation to leave them undefined - accessing them is malicious behavior.
512
+ exports: originalModuleRecord.exports,
513
+ execute: (moduleExports, compartment, resolvedImports) => {
514
+ const ns = {};
515
+ originalModuleRecord.execute(ns, compartment, resolvedImports);
516
+ const attenuated = attenuate(ns);
517
+ moduleExports.default = attenuated;
518
+ assign(moduleExports, attenuated);
519
+ },
520
+ }),
521
+ );
470
522
  }
471
523
 
472
524
  /**
473
525
  * Throws if importing of the specifier is not allowed by the policy
474
526
  *
475
527
  * @param {string} specifier - exit module name
476
- * @param {import('ses').ThirdPartyStaticModuleInterface} originalModuleRecord - reference to the exit module
477
- * @param {PackagePolicy} policy - local compartment policy
528
+ * @param {ThirdPartyStaticModuleInterface} originalModuleRecord - reference to the exit module
529
+ * @param {PackagePolicy|undefined} policy - local compartment policy
478
530
  * @param {DeferredAttenuatorsProvider} attenuators - a key-value where attenuations can be found
479
- * @returns {Promise<import('ses').ThirdPartyStaticModuleInterface>} - the attenuated module
531
+ * @returns {Promise<ThirdPartyStaticModuleInterface>} - the attenuated module
480
532
  */
481
533
  export const attenuateModuleHook = async (
482
534
  specifier,
@@ -484,8 +536,11 @@ export const attenuateModuleHook = async (
484
536
  policy,
485
537
  attenuators,
486
538
  ) => {
539
+ if (!policy) {
540
+ return originalModuleRecord;
541
+ }
487
542
  const policyValue = policyLookupHelper(policy, 'builtins', specifier);
488
- if (!policy || policyValue === true) {
543
+ if (policyValue === true) {
489
544
  return originalModuleRecord;
490
545
  }
491
546
 
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Fairly exhaustive, excruciatingly pedantic _type-level_ helpers for
3
+ * representing and validating **Canonical Names** and npm package names.
4
+ *
5
+ * A {@link CanonicalName | Canonical Name} is a string containing one or more
6
+ * npm package names (scoped or unscoped) delimited by a `>` character.
7
+ *
8
+ * The following rules about npm package names are enforced:
9
+ *
10
+ * - ✅ Length > 0
11
+ * - ✅ Can contain hyphens
12
+ * - ✅ No leading `.` or `_`
13
+ * - ✅ No spaces
14
+ * - ✅ No `~)('!*` characters
15
+ *
16
+ * The following rules are not enforced:
17
+ *
18
+ * - ❌ All lowercase - Not feasible due to recursion limits & legacy package
19
+ * names
20
+ * - ❌ Not a reserved name - unmaintainable list of node builtin module names
21
+ * - ❌ Length ≤ 214 - Not feasible due to recursion limits & legacy package
22
+ * names
23
+ *
24
+ * "Legacy" package names may contain uppercase letters and be longer than 214
25
+ * characters.
26
+ *
27
+ * @module
28
+ * @see {@link https://www.npmjs.com/package/validate-npm-package-name}
29
+ */
30
+ /**
31
+ * Characters that are explicitly forbidden in npm package names. These include:
32
+ * ` `, `~`, `)`, `(`, `'`, `!`, `*`
33
+ *
34
+ * We check each one individually because TypeScript's template literal types
35
+ * can detect if a string contains a specific substring.
36
+ *
37
+ * Returns `true` if the string contains a forbidden character, `false`
38
+ * otherwise.
39
+ */
40
+ type ContainsForbiddenChar<S extends string> = S extends `${string} ${string}` | `${string}~${string}` | `${string})${string}` | `${string}(${string}` | `${string}'${string}` | `${string}!${string}` | `${string}*${string}` ? true : false;
41
+ /**
42
+ * Validates that a string doesn't start with `.` or `_`.
43
+ *
44
+ * Returns `true` if the string doesn't start with `.` or `_`, `false`
45
+ * otherwise.
46
+ */
47
+ type HasValidStart<S extends string> = S extends `.${string}` | `_${string}` ? false : true;
48
+ /**
49
+ * Validates that a string is non-empty.
50
+ *
51
+ * Returns `true` if the string is non-empty, `false` otherwise.
52
+ */
53
+ type IsNonEmpty<S extends string> = S extends '' ? false : true;
54
+ /**
55
+ * Combines all validation checks for a package name segment.
56
+ *
57
+ * Returns `true` if the string passes all checks, `false` otherwise.
58
+ */
59
+ type IsValidPackageNameSegment<S extends string> = IsNonEmpty<S> extends false ? false : HasValidStart<S> extends false ? false : ContainsForbiddenChar<S> extends true ? false : true;
60
+ /** A scoped npm package name, like "@scope/pkg" */
61
+ export type ScopedPackageName<S extends string = string> = S extends `@${infer Scope}/${infer Name}` ? IsValidPackageNameSegment<Scope> extends true ? IsValidPackageNameSegment<Name> extends true ? S : never : never : never;
62
+ /**
63
+ * An unscoped npm package name.
64
+ *
65
+ * Must pass all validation checks and must not contain a `/` (which would
66
+ * indicate a scoped package or a subpath).
67
+ *
68
+ * Note: Package names containing uppercase letters are technically invalid per
69
+ * npm rules, but they exist in the wild. TypeScript cannot reliably validate
70
+ * case at the type level, so we don't enforce this.
71
+ */
72
+ export type UnscopedPackageName<S extends string = string> = S extends `${string}/${string}` ? never : IsValidPackageNameSegment<S> extends true ? S : never;
73
+ /**
74
+ * A scoped or unscoped npm package name.
75
+ */
76
+ export type NpmPackageName<S extends string = string> = S extends `@${string}/${string}` ? ScopedPackageName<S> : UnscopedPackageName<S>;
77
+ /**
78
+ * Split a string on `>`—the canonical name delimiter—into a tuple of segments.
79
+ */
80
+ export type SplitOnDelimiter<S extends string> = S extends `${infer Head}>${infer Tail}` ? [Head, ...SplitOnDelimiter<Tail>] : [S];
81
+ /**
82
+ * Validate that every element in a tuple of strings is a valid npm package
83
+ * name.
84
+ */
85
+ export type AllValidPackageNames<Parts extends readonly string[]> = Parts extends [
86
+ infer Head extends string,
87
+ ...infer Tail extends readonly string[]
88
+ ] ? NpmPackageName<Head> extends never ? never : AllValidPackageNames<Tail> : Parts;
89
+ /**
90
+ * A Canonical Name string comprised of one or more npm package names separated
91
+ * by `>` (e.g., `foo`, `@scope/foo>bar`, `foo>@scope/bar>baz`).
92
+ *
93
+ * When given a string literal type, invalid shapes narrow to `never`.
94
+ */
95
+ export type CanonicalName<S extends string = string> = AllValidPackageNames<SplitOnDelimiter<S>> extends never ? never : S;
96
+ export {};
97
+ //# sourceMappingURL=canonical-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-name.d.ts","sourceRoot":"","sources":["canonical-name.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;GASG;AACH,KAAK,qBAAqB,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAC5C,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,GAAG,MAAM,IAAI,MAAM,EAAE,GACrB,IAAI,GACJ,KAAK,CAAC;AAEV;;;;;GAKG;AACH,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,IAAI,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,GACxE,KAAK,GACL,IAAI,CAAC;AAET;;;;GAIG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC;AAEhE;;;;GAIG;AACH,KAAK,yBAAyB,CAAC,CAAC,SAAS,MAAM,IAC7C,UAAU,CAAC,CAAC,CAAC,SAAS,KAAK,GACvB,KAAK,GACL,aAAa,CAAC,CAAC,CAAC,SAAS,KAAK,GAC5B,KAAK,GACL,qBAAqB,CAAC,CAAC,CAAC,SAAS,IAAI,GACnC,KAAK,GACL,IAAI,CAAC;AAMf,mDAAmD;AACnD,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IACrD,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI,MAAM,IAAI,EAAE,GACrC,yBAAyB,CAAC,KAAK,CAAC,SAAS,IAAI,GAC3C,yBAAyB,CAAC,IAAI,CAAC,SAAS,IAAI,GAC1C,CAAC,GACD,KAAK,GACP,KAAK,GACP,KAAK,CAAC;AAEZ;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IACvD,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,GAC3B,KAAK,GACL,yBAAyB,CAAC,CAAC,CAAC,SAAS,IAAI,GACvC,CAAC,GACD,KAAK,CAAC;AAEd;;GAEG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAClD,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,EAAE,GAC5B,iBAAiB,CAAC,CAAC,CAAC,GACpB,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE7B;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,IAC3C,CAAC,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GACnC,CAAC,IAAI,EAAE,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,GACjC,CAAC,CAAC,CAAC,CAAC;AAEV;;;GAGG;AACH,MAAM,MAAM,oBAAoB,CAAC,KAAK,SAAS,SAAS,MAAM,EAAE,IAC9D,KAAK,SAAS;IACZ,MAAM,IAAI,SAAS,MAAM;IACzB,GAAG,MAAM,IAAI,SAAS,SAAS,MAAM,EAAE;CACxC,GACG,cAAc,CAAC,IAAI,CAAC,SAAS,KAAK,GAChC,KAAK,GACL,oBAAoB,CAAC,IAAI,CAAC,GAC5B,KAAK,CAAC;AAEZ;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IACjD,oBAAoB,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Fairly exhaustive, excruciatingly pedantic _type-level_ helpers for
3
+ * representing and validating **Canonical Names** and npm package names.
4
+ *
5
+ * A {@link CanonicalName | Canonical Name} is a string containing one or more
6
+ * npm package names (scoped or unscoped) delimited by a `>` character.
7
+ *
8
+ * The following rules about npm package names are enforced:
9
+ *
10
+ * - ✅ Length > 0
11
+ * - ✅ Can contain hyphens
12
+ * - ✅ No leading `.` or `_`
13
+ * - ✅ No spaces
14
+ * - ✅ No `~)('!*` characters
15
+ *
16
+ * The following rules are not enforced:
17
+ *
18
+ * - ❌ All lowercase - Not feasible due to recursion limits & legacy package
19
+ * names
20
+ * - ❌ Not a reserved name - unmaintainable list of node builtin module names
21
+ * - ❌ Length ≤ 214 - Not feasible due to recursion limits & legacy package
22
+ * names
23
+ *
24
+ * "Legacy" package names may contain uppercase letters and be longer than 214
25
+ * characters.
26
+ *
27
+ * @module
28
+ * @see {@link https://www.npmjs.com/package/validate-npm-package-name}
29
+ */
30
+
31
+ /**
32
+ * Characters that are explicitly forbidden in npm package names. These include:
33
+ * ` `, `~`, `)`, `(`, `'`, `!`, `*`
34
+ *
35
+ * We check each one individually because TypeScript's template literal types
36
+ * can detect if a string contains a specific substring.
37
+ *
38
+ * Returns `true` if the string contains a forbidden character, `false`
39
+ * otherwise.
40
+ */
41
+ type ContainsForbiddenChar<S extends string> = S extends
42
+ | `${string} ${string}`
43
+ | `${string}~${string}`
44
+ | `${string})${string}`
45
+ | `${string}(${string}`
46
+ | `${string}'${string}`
47
+ | `${string}!${string}`
48
+ | `${string}*${string}`
49
+ ? true
50
+ : false;
51
+
52
+ /**
53
+ * Validates that a string doesn't start with `.` or `_`.
54
+ *
55
+ * Returns `true` if the string doesn't start with `.` or `_`, `false`
56
+ * otherwise.
57
+ */
58
+ type HasValidStart<S extends string> = S extends `.${string}` | `_${string}`
59
+ ? false
60
+ : true;
61
+
62
+ /**
63
+ * Validates that a string is non-empty.
64
+ *
65
+ * Returns `true` if the string is non-empty, `false` otherwise.
66
+ */
67
+ type IsNonEmpty<S extends string> = S extends '' ? false : true;
68
+
69
+ /**
70
+ * Combines all validation checks for a package name segment.
71
+ *
72
+ * Returns `true` if the string passes all checks, `false` otherwise.
73
+ */
74
+ type IsValidPackageNameSegment<S extends string> =
75
+ IsNonEmpty<S> extends false
76
+ ? false
77
+ : HasValidStart<S> extends false
78
+ ? false
79
+ : ContainsForbiddenChar<S> extends true
80
+ ? false
81
+ : true;
82
+
83
+ // ============================================================================
84
+ // Scoped and Unscoped Package Names
85
+ // ============================================================================
86
+
87
+ /** A scoped npm package name, like "@scope/pkg" */
88
+ export type ScopedPackageName<S extends string = string> =
89
+ S extends `@${infer Scope}/${infer Name}`
90
+ ? IsValidPackageNameSegment<Scope> extends true
91
+ ? IsValidPackageNameSegment<Name> extends true
92
+ ? S
93
+ : never
94
+ : never
95
+ : never;
96
+
97
+ /**
98
+ * An unscoped npm package name.
99
+ *
100
+ * Must pass all validation checks and must not contain a `/` (which would
101
+ * indicate a scoped package or a subpath).
102
+ *
103
+ * Note: Package names containing uppercase letters are technically invalid per
104
+ * npm rules, but they exist in the wild. TypeScript cannot reliably validate
105
+ * case at the type level, so we don't enforce this.
106
+ */
107
+ export type UnscopedPackageName<S extends string = string> =
108
+ S extends `${string}/${string}`
109
+ ? never
110
+ : IsValidPackageNameSegment<S> extends true
111
+ ? S
112
+ : never;
113
+
114
+ /**
115
+ * A scoped or unscoped npm package name.
116
+ */
117
+ export type NpmPackageName<S extends string = string> =
118
+ S extends `@${string}/${string}`
119
+ ? ScopedPackageName<S>
120
+ : UnscopedPackageName<S>;
121
+
122
+ /**
123
+ * Split a string on `>`—the canonical name delimiter—into a tuple of segments.
124
+ */
125
+ export type SplitOnDelimiter<S extends string> =
126
+ S extends `${infer Head}>${infer Tail}`
127
+ ? [Head, ...SplitOnDelimiter<Tail>]
128
+ : [S];
129
+
130
+ /**
131
+ * Validate that every element in a tuple of strings is a valid npm package
132
+ * name.
133
+ */
134
+ export type AllValidPackageNames<Parts extends readonly string[]> =
135
+ Parts extends [
136
+ infer Head extends string,
137
+ ...infer Tail extends readonly string[],
138
+ ]
139
+ ? NpmPackageName<Head> extends never
140
+ ? never
141
+ : AllValidPackageNames<Tail>
142
+ : Parts;
143
+
144
+ /**
145
+ * A Canonical Name string comprised of one or more npm package names separated
146
+ * by `>` (e.g., `foo`, `@scope/foo>bar`, `foo>@scope/bar>baz`).
147
+ *
148
+ * When given a string literal type, invalid shapes narrow to `never`.
149
+ */
150
+ export type CanonicalName<S extends string = string> =
151
+ AllValidPackageNames<SplitOnDelimiter<S>> extends never ? never : S;