@xemahq/xema-npm-auth-plugin 0.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/LICENSE +201 -0
- package/README.md +60 -0
- package/dist/acl.d.ts +11 -0
- package/dist/acl.d.ts.map +1 -0
- package/dist/acl.js +123 -0
- package/dist/acl.js.map +1 -0
- package/dist/jwks.d.ts +28 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +175 -0
- package/dist/jwks.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +36 -0
- package/dist/logger.js.map +1 -0
- package/dist/plugin.cjs +646 -0
- package/dist/plugin.d.ts +26 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +197 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +33 -0
package/dist/logger.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adaptVerdaccioLogger = adaptVerdaccioLogger;
|
|
4
|
+
exports.newCorrelationId = newCorrelationId;
|
|
5
|
+
function adaptVerdaccioLogger(verdaccioLogger) {
|
|
6
|
+
const v = verdaccioLogger;
|
|
7
|
+
const level = (key) => {
|
|
8
|
+
const fn = v?.[key];
|
|
9
|
+
if (typeof fn === 'function') {
|
|
10
|
+
return (msg, meta) => fn(meta ?? {}, `[xema-jwt] ${msg}`);
|
|
11
|
+
}
|
|
12
|
+
return (msg, meta) => {
|
|
13
|
+
const out = meta ? `[xema-jwt] ${msg} ${JSON.stringify(meta)}` : `[xema-jwt] ${msg}`;
|
|
14
|
+
if (key === 'error') {
|
|
15
|
+
console.error(out);
|
|
16
|
+
}
|
|
17
|
+
else if (key === 'warn') {
|
|
18
|
+
console.warn(out);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log(out);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
info: level('info'),
|
|
27
|
+
warn: level('warn'),
|
|
28
|
+
error: level('error'),
|
|
29
|
+
debug: level('debug'),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function newCorrelationId() {
|
|
33
|
+
const { randomBytes } = require('node:crypto');
|
|
34
|
+
return randomBytes(6).toString('hex');
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":";;AAkBA,oDA0BC;AAOD,4CAKC;AAtCD,SAAgB,oBAAoB,CAAC,eAAwB;IAC3D,MAAM,CAAC,GAAG,eAAkG,CAAC;IAC7G,MAAM,KAAK,GAAG,CACZ,GAAwC,EACiB,EAAE;QAC3D,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QACpB,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,cAAc,GAAG,EAAE,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YACnB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,EAAE,CAAC;YACrF,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;gBACpB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;iBAAM,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;IACF,OAAO;QACL,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC;QACnB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC;QACrB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC;KACtB,CAAC;AACJ,CAAC;AAOD,SAAgB,gBAAgB;IAG9B,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,aAAa,CAAiC,CAAC;IAC/E,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC"}
|
package/dist/plugin.cjs
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// ../../../../xema-kernel-contracts/packages/kernel/kernel-contracts/dist/project-kit/lib/project-kit.js
|
|
35
|
+
var require_project_kit = __commonJS({
|
|
36
|
+
"../../../../xema-kernel-contracts/packages/kernel/kernel-contracts/dist/project-kit/lib/project-kit.js"(exports2) {
|
|
37
|
+
"use strict";
|
|
38
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
39
|
+
exports2.ProjectKitBindingKind = exports2.RegistryVisibilityTier = exports2.ProjectKitSourceKind = exports2.ProjectKitPurpose = exports2.ProjectKitKind = void 0;
|
|
40
|
+
var ProjectKitKind;
|
|
41
|
+
(function(ProjectKitKind2) {
|
|
42
|
+
ProjectKitKind2["NpmDependency"] = "npm-dependency";
|
|
43
|
+
ProjectKitKind2["ShadcnRegistry"] = "shadcn-registry";
|
|
44
|
+
})(ProjectKitKind || (exports2.ProjectKitKind = ProjectKitKind = {}));
|
|
45
|
+
var ProjectKitPurpose;
|
|
46
|
+
(function(ProjectKitPurpose2) {
|
|
47
|
+
ProjectKitPurpose2["ComponentLibrary"] = "component-library";
|
|
48
|
+
ProjectKitPurpose2["GenericDependency"] = "generic-dependency";
|
|
49
|
+
})(ProjectKitPurpose || (exports2.ProjectKitPurpose = ProjectKitPurpose = {}));
|
|
50
|
+
var ProjectKitSourceKind;
|
|
51
|
+
(function(ProjectKitSourceKind2) {
|
|
52
|
+
ProjectKitSourceKind2["NpmRegistry"] = "npm-registry";
|
|
53
|
+
ProjectKitSourceKind2["Git"] = "git";
|
|
54
|
+
ProjectKitSourceKind2["ShadcnRegistry"] = "shadcn-registry";
|
|
55
|
+
})(ProjectKitSourceKind || (exports2.ProjectKitSourceKind = ProjectKitSourceKind = {}));
|
|
56
|
+
var RegistryVisibilityTier2;
|
|
57
|
+
(function(RegistryVisibilityTier3) {
|
|
58
|
+
RegistryVisibilityTier3["System"] = "system";
|
|
59
|
+
RegistryVisibilityTier3["SystemShareable"] = "system-shareable";
|
|
60
|
+
RegistryVisibilityTier3["Org"] = "org";
|
|
61
|
+
RegistryVisibilityTier3["External"] = "external";
|
|
62
|
+
})(RegistryVisibilityTier2 || (exports2.RegistryVisibilityTier = RegistryVisibilityTier2 = {}));
|
|
63
|
+
var ProjectKitBindingKind;
|
|
64
|
+
(function(ProjectKitBindingKind2) {
|
|
65
|
+
ProjectKitBindingKind2["Catalog"] = "catalog";
|
|
66
|
+
ProjectKitBindingKind2["Custom"] = "custom";
|
|
67
|
+
})(ProjectKitBindingKind || (exports2.ProjectKitBindingKind = ProjectKitBindingKind = {}));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ../../../../xema-kernel-contracts/packages/kernel/kernel-contracts/dist/project-kit/index.js
|
|
72
|
+
var require_project_kit2 = __commonJS({
|
|
73
|
+
"../../../../xema-kernel-contracts/packages/kernel/kernel-contracts/dist/project-kit/index.js"(exports2) {
|
|
74
|
+
"use strict";
|
|
75
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
76
|
+
if (k2 === void 0) k2 = k;
|
|
77
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
78
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
79
|
+
desc = { enumerable: true, get: /* @__PURE__ */ __name(function() {
|
|
80
|
+
return m[k];
|
|
81
|
+
}, "get") };
|
|
82
|
+
}
|
|
83
|
+
Object.defineProperty(o, k2, desc);
|
|
84
|
+
}) : (function(o, m, k, k2) {
|
|
85
|
+
if (k2 === void 0) k2 = k;
|
|
86
|
+
o[k2] = m[k];
|
|
87
|
+
}));
|
|
88
|
+
var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
|
|
89
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
90
|
+
};
|
|
91
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
92
|
+
__exportStar(require_project_kit(), exports2);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/plugin.ts
|
|
97
|
+
var plugin_exports = {};
|
|
98
|
+
__export(plugin_exports, {
|
|
99
|
+
AclDenyReason: () => AclDenyReason,
|
|
100
|
+
JwtVerifier: () => JwtVerifier,
|
|
101
|
+
PackageTier: () => PackageTier,
|
|
102
|
+
XemaRegistryRole: () => XemaRegistryRole,
|
|
103
|
+
__buildBiomeForTests: () => buildPlugin,
|
|
104
|
+
buildPlugin: () => buildPlugin,
|
|
105
|
+
classifyPackage: () => classifyPackage,
|
|
106
|
+
default: () => plugin_default,
|
|
107
|
+
evaluateAcl: () => evaluateAcl,
|
|
108
|
+
packageTierToVisibility: () => packageTierToVisibility,
|
|
109
|
+
pluginFactory: () => pluginFactory
|
|
110
|
+
});
|
|
111
|
+
module.exports = __toCommonJS(plugin_exports);
|
|
112
|
+
|
|
113
|
+
// src/acl.ts
|
|
114
|
+
var import_project_kit = __toESM(require_project_kit2());
|
|
115
|
+
|
|
116
|
+
// src/types.ts
|
|
117
|
+
var XemaRegistryRole = /* @__PURE__ */ ((XemaRegistryRole2) => {
|
|
118
|
+
XemaRegistryRole2["Platform"] = "platform";
|
|
119
|
+
XemaRegistryRole2["OrgAdmin"] = "org_admin";
|
|
120
|
+
XemaRegistryRole2["OrgMember"] = "org_member";
|
|
121
|
+
XemaRegistryRole2["Anonymous"] = "anonymous";
|
|
122
|
+
return XemaRegistryRole2;
|
|
123
|
+
})(XemaRegistryRole || {});
|
|
124
|
+
var AclDenyReason = /* @__PURE__ */ ((AclDenyReason3) => {
|
|
125
|
+
AclDenyReason3["MissingJwt"] = "missing_jwt";
|
|
126
|
+
AclDenyReason3["InvalidJwt"] = "invalid_jwt";
|
|
127
|
+
AclDenyReason3["ExpiredJwt"] = "expired_jwt";
|
|
128
|
+
AclDenyReason3["WrongRoleForSystem"] = "wrong_role_for_system";
|
|
129
|
+
AclDenyReason3["WrongRoleForSystemShareablePublish"] = "wrong_role_for_system_shareable_publish";
|
|
130
|
+
AclDenyReason3["AnonymousOrgRead"] = "anonymous_org_read";
|
|
131
|
+
AclDenyReason3["CrossOrgAccess"] = "cross_org_access";
|
|
132
|
+
AclDenyReason3["UnknownPackagePattern"] = "unknown_package_pattern";
|
|
133
|
+
return AclDenyReason3;
|
|
134
|
+
})(AclDenyReason || {});
|
|
135
|
+
var PackageTier = /* @__PURE__ */ ((PackageTier2) => {
|
|
136
|
+
PackageTier2["System"] = "system";
|
|
137
|
+
PackageTier2["SystemShareable"] = "system_shareable";
|
|
138
|
+
PackageTier2["Org"] = "org";
|
|
139
|
+
PackageTier2["Uplink"] = "uplink";
|
|
140
|
+
return PackageTier2;
|
|
141
|
+
})(PackageTier || {});
|
|
142
|
+
|
|
143
|
+
// src/acl.ts
|
|
144
|
+
var SYSTEM_SCOPE = "@xema-system";
|
|
145
|
+
var SHAREABLE_SCOPE = "@xema-shareable";
|
|
146
|
+
function packageTierToVisibility(tier) {
|
|
147
|
+
switch (tier) {
|
|
148
|
+
case "system" /* System */:
|
|
149
|
+
return import_project_kit.RegistryVisibilityTier.System;
|
|
150
|
+
case "system_shareable" /* SystemShareable */:
|
|
151
|
+
return import_project_kit.RegistryVisibilityTier.SystemShareable;
|
|
152
|
+
case "org" /* Org */:
|
|
153
|
+
return import_project_kit.RegistryVisibilityTier.Org;
|
|
154
|
+
case "uplink" /* Uplink */:
|
|
155
|
+
return import_project_kit.RegistryVisibilityTier.External;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
__name(packageTierToVisibility, "packageTierToVisibility");
|
|
159
|
+
function classifyPackage(packageName) {
|
|
160
|
+
if (packageName.startsWith("@")) {
|
|
161
|
+
const slashIdx = packageName.indexOf("/");
|
|
162
|
+
if (slashIdx <= 1) {
|
|
163
|
+
return { tier: "uplink" /* Uplink */, orgScope: null, raw: packageName };
|
|
164
|
+
}
|
|
165
|
+
const scope = packageName.slice(0, slashIdx);
|
|
166
|
+
if (scope === SYSTEM_SCOPE) {
|
|
167
|
+
return { tier: "system" /* System */, orgScope: null, raw: packageName };
|
|
168
|
+
}
|
|
169
|
+
if (scope === SHAREABLE_SCOPE) {
|
|
170
|
+
return { tier: "system_shareable" /* SystemShareable */, orgScope: null, raw: packageName };
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
tier: "org" /* Org */,
|
|
174
|
+
orgScope: scope.slice(1),
|
|
175
|
+
// drop leading '@'
|
|
176
|
+
raw: packageName
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { tier: "uplink" /* Uplink */, orgScope: null, raw: packageName };
|
|
180
|
+
}
|
|
181
|
+
__name(classifyPackage, "classifyPackage");
|
|
182
|
+
function evaluateAcl(input) {
|
|
183
|
+
const pkg = classifyPackage(input.packageName);
|
|
184
|
+
const isPublish = input.action === "publish";
|
|
185
|
+
switch (pkg.tier) {
|
|
186
|
+
case "system" /* System */: {
|
|
187
|
+
if (!input.claims) {
|
|
188
|
+
return {
|
|
189
|
+
allow: false,
|
|
190
|
+
reason: "missing_jwt" /* MissingJwt */,
|
|
191
|
+
message: `system tier (${pkg.raw}) requires platform JWT`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (input.claims.x_role !== "platform" /* Platform */) {
|
|
195
|
+
return {
|
|
196
|
+
allow: false,
|
|
197
|
+
reason: "wrong_role_for_system" /* WrongRoleForSystem */,
|
|
198
|
+
message: `system tier (${pkg.raw}) requires role=platform, got role=${input.claims.x_role}`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { allow: true };
|
|
202
|
+
}
|
|
203
|
+
case "system_shareable" /* SystemShareable */: {
|
|
204
|
+
if (!isPublish) {
|
|
205
|
+
return { allow: true };
|
|
206
|
+
}
|
|
207
|
+
if (!input.claims) {
|
|
208
|
+
return {
|
|
209
|
+
allow: false,
|
|
210
|
+
reason: "missing_jwt" /* MissingJwt */,
|
|
211
|
+
message: `system-shareable publish (${pkg.raw}) requires platform JWT`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (input.claims.x_role !== "platform" /* Platform */) {
|
|
215
|
+
return {
|
|
216
|
+
allow: false,
|
|
217
|
+
reason: "wrong_role_for_system_shareable_publish" /* WrongRoleForSystemShareablePublish */,
|
|
218
|
+
message: `system-shareable publish (${pkg.raw}) requires role=platform, got role=${input.claims.x_role}`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { allow: true };
|
|
222
|
+
}
|
|
223
|
+
case "org" /* Org */: {
|
|
224
|
+
if (!input.claims) {
|
|
225
|
+
return {
|
|
226
|
+
allow: false,
|
|
227
|
+
reason: "anonymous_org_read" /* AnonymousOrgRead */,
|
|
228
|
+
message: `org tier (${pkg.raw}) requires authentication`
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (input.claims.x_role === "platform" /* Platform */) {
|
|
232
|
+
return { allow: true };
|
|
233
|
+
}
|
|
234
|
+
if (input.claims.x_role === "anonymous" /* Anonymous */) {
|
|
235
|
+
return {
|
|
236
|
+
allow: false,
|
|
237
|
+
reason: "anonymous_org_read" /* AnonymousOrgRead */,
|
|
238
|
+
message: `org tier (${pkg.raw}) denied for role=anonymous`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (pkg.orgScope !== input.claims.x_org_id) {
|
|
242
|
+
return {
|
|
243
|
+
allow: false,
|
|
244
|
+
reason: "cross_org_access" /* CrossOrgAccess */,
|
|
245
|
+
message: `package scope "@${pkg.orgScope}" does not match jwt x_org_id "${input.claims.x_org_id}"`
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { allow: true };
|
|
249
|
+
}
|
|
250
|
+
case "uplink" /* Uplink */: {
|
|
251
|
+
if (isPublish) {
|
|
252
|
+
return {
|
|
253
|
+
allow: false,
|
|
254
|
+
reason: "unknown_package_pattern" /* UnknownPackagePattern */,
|
|
255
|
+
message: `cannot publish unscoped package "${pkg.raw}" \u2014 only scoped tiers accept writes`
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { allow: true };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
__name(evaluateAcl, "evaluateAcl");
|
|
263
|
+
|
|
264
|
+
// src/jwks.ts
|
|
265
|
+
var import_node_crypto = require("node:crypto");
|
|
266
|
+
var ALLOWED_ALG = /* @__PURE__ */ new Set(["RS256", "RS384", "RS512"]);
|
|
267
|
+
var ALG_TO_DIGEST = {
|
|
268
|
+
RS256: "sha256",
|
|
269
|
+
RS384: "sha384",
|
|
270
|
+
RS512: "sha512"
|
|
271
|
+
};
|
|
272
|
+
var JwtVerifier = class {
|
|
273
|
+
static {
|
|
274
|
+
__name(this, "JwtVerifier");
|
|
275
|
+
}
|
|
276
|
+
jwksUrl;
|
|
277
|
+
expectedIssuer;
|
|
278
|
+
refreshMs;
|
|
279
|
+
fetchFn;
|
|
280
|
+
logger;
|
|
281
|
+
now;
|
|
282
|
+
keys = /* @__PURE__ */ new Map();
|
|
283
|
+
lastRefreshedAt = 0;
|
|
284
|
+
inflight = null;
|
|
285
|
+
constructor(opts) {
|
|
286
|
+
if (!opts.jwksUrl) {
|
|
287
|
+
throw new Error("JwtVerifier requires jwksUrl");
|
|
288
|
+
}
|
|
289
|
+
this.jwksUrl = opts.jwksUrl;
|
|
290
|
+
this.expectedIssuer = opts.expectedIssuer;
|
|
291
|
+
this.refreshMs = opts.refreshMs ?? 10 * 60 * 1e3;
|
|
292
|
+
this.fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
293
|
+
if (typeof this.fetchFn !== "function") {
|
|
294
|
+
throw new Error("JwtVerifier requires a fetch implementation");
|
|
295
|
+
}
|
|
296
|
+
this.logger = opts.logger;
|
|
297
|
+
this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Verify a bearer token. Returns the decoded {@link XemaJwtClaims} on
|
|
301
|
+
* success. Throws on ANY failure — caller MUST catch and map to a deny.
|
|
302
|
+
*/
|
|
303
|
+
async verify(token) {
|
|
304
|
+
const parts = token.split(".");
|
|
305
|
+
if (parts.length !== 3) {
|
|
306
|
+
throw new Error("JWT is not a three-segment compact-serialization token");
|
|
307
|
+
}
|
|
308
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
309
|
+
const header = decodeJsonSegment(headerB64);
|
|
310
|
+
if (!ALLOWED_ALG.has(header.alg)) {
|
|
311
|
+
throw new Error(`Unsupported JWT alg "${header.alg}" (only RS256/RS384/RS512 accepted)`);
|
|
312
|
+
}
|
|
313
|
+
if (!header.kid) {
|
|
314
|
+
throw new Error('JWT header missing "kid"');
|
|
315
|
+
}
|
|
316
|
+
await this.ensureKeysFresh();
|
|
317
|
+
let key = this.keys.get(header.kid);
|
|
318
|
+
if (!key) {
|
|
319
|
+
await this.refreshNow();
|
|
320
|
+
key = this.keys.get(header.kid);
|
|
321
|
+
if (!key) {
|
|
322
|
+
throw new Error(`JWT kid "${header.kid}" not found in JWKS`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
326
|
+
const signature = Buffer.from(signatureB64, "base64url");
|
|
327
|
+
const verifier = (0, import_node_crypto.createVerify)(ALG_TO_DIGEST[header.alg]);
|
|
328
|
+
verifier.update(signingInput);
|
|
329
|
+
verifier.end();
|
|
330
|
+
if (!verifier.verify(key, signature)) {
|
|
331
|
+
throw new Error("JWT signature did not verify against any JWKS key");
|
|
332
|
+
}
|
|
333
|
+
const payload = decodeJsonSegment(payloadB64);
|
|
334
|
+
const claims = this.assertClaims(payload);
|
|
335
|
+
if (this.expectedIssuer && payload["iss"] !== this.expectedIssuer) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`JWT iss "${String(payload["iss"])}" does not match expected "${this.expectedIssuer}"`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return claims;
|
|
341
|
+
}
|
|
342
|
+
/** Force a synchronous refresh — used by tests and on kid miss. */
|
|
343
|
+
async refreshNow() {
|
|
344
|
+
if (this.inflight) {
|
|
345
|
+
await this.inflight;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.inflight = this.doRefresh().finally(() => {
|
|
349
|
+
this.inflight = null;
|
|
350
|
+
});
|
|
351
|
+
await this.inflight;
|
|
352
|
+
}
|
|
353
|
+
async ensureKeysFresh() {
|
|
354
|
+
const ageMs = Date.now() - this.lastRefreshedAt;
|
|
355
|
+
if (this.keys.size > 0 && ageMs < this.refreshMs) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
await this.refreshNow();
|
|
359
|
+
}
|
|
360
|
+
async doRefresh() {
|
|
361
|
+
this.logger.debug("JWKS refresh start", { jwksUrl: this.jwksUrl });
|
|
362
|
+
const res = await this.fetchFn(this.jwksUrl, {
|
|
363
|
+
headers: { Accept: "application/json" }
|
|
364
|
+
});
|
|
365
|
+
if (!res.ok) {
|
|
366
|
+
throw new Error(`JWKS fetch failed: ${res.status} ${res.statusText}`);
|
|
367
|
+
}
|
|
368
|
+
const doc = await res.json();
|
|
369
|
+
if (!doc || !Array.isArray(doc.keys)) {
|
|
370
|
+
throw new Error('JWKS response is missing "keys" array');
|
|
371
|
+
}
|
|
372
|
+
const next = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const k of doc.keys) {
|
|
374
|
+
if (k.kty !== "RSA" || !k.kid || !k.n || !k.e) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const keyObj = (0, import_node_crypto.createPublicKey)({
|
|
379
|
+
key: {
|
|
380
|
+
kty: "RSA",
|
|
381
|
+
n: k.n,
|
|
382
|
+
e: k.e
|
|
383
|
+
},
|
|
384
|
+
format: "jwk"
|
|
385
|
+
});
|
|
386
|
+
next.set(k.kid, keyObj);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
this.logger.warn("JWKS key skipped (createPublicKey failed)", {
|
|
389
|
+
kid: k.kid,
|
|
390
|
+
error: err.message
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (next.size === 0) {
|
|
395
|
+
throw new Error("JWKS contained zero usable RSA keys");
|
|
396
|
+
}
|
|
397
|
+
this.keys = next;
|
|
398
|
+
this.lastRefreshedAt = Date.now();
|
|
399
|
+
this.logger.info("JWKS refresh ok", { keyCount: next.size });
|
|
400
|
+
}
|
|
401
|
+
assertClaims(payload) {
|
|
402
|
+
const exp = payload["exp"];
|
|
403
|
+
if (typeof exp !== "number") {
|
|
404
|
+
throw new Error('JWT missing numeric "exp"');
|
|
405
|
+
}
|
|
406
|
+
if (exp <= this.now()) {
|
|
407
|
+
throw new Error("JWT is expired");
|
|
408
|
+
}
|
|
409
|
+
const sub = payload["sub"];
|
|
410
|
+
if (typeof sub !== "string" || sub.length === 0) {
|
|
411
|
+
throw new Error('JWT missing "sub"');
|
|
412
|
+
}
|
|
413
|
+
const orgId = payload["x_org_id"];
|
|
414
|
+
if (typeof orgId !== "string" || orgId.length === 0) {
|
|
415
|
+
throw new Error('JWT missing "x_org_id" claim');
|
|
416
|
+
}
|
|
417
|
+
const role = payload["x_role"];
|
|
418
|
+
if (typeof role !== "string" || !isXemaRole(role)) {
|
|
419
|
+
throw new Error(`JWT "x_role" must be one of platform|org_admin|org_member|anonymous (got "${String(role)}")`);
|
|
420
|
+
}
|
|
421
|
+
const claims = {
|
|
422
|
+
sub,
|
|
423
|
+
exp,
|
|
424
|
+
x_org_id: orgId,
|
|
425
|
+
x_role: role
|
|
426
|
+
};
|
|
427
|
+
if (typeof payload["iat"] === "number") {
|
|
428
|
+
claims.iat = payload["iat"];
|
|
429
|
+
}
|
|
430
|
+
return claims;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
function decodeJsonSegment(segment) {
|
|
434
|
+
const buf = Buffer.from(segment, "base64url");
|
|
435
|
+
return JSON.parse(buf.toString("utf8"));
|
|
436
|
+
}
|
|
437
|
+
__name(decodeJsonSegment, "decodeJsonSegment");
|
|
438
|
+
function isXemaRole(value) {
|
|
439
|
+
return value === "platform" /* Platform */ || value === "org_admin" /* OrgAdmin */ || value === "org_member" /* OrgMember */ || value === "anonymous" /* Anonymous */;
|
|
440
|
+
}
|
|
441
|
+
__name(isXemaRole, "isXemaRole");
|
|
442
|
+
|
|
443
|
+
// src/logger.ts
|
|
444
|
+
function adaptVerdaccioLogger(verdaccioLogger) {
|
|
445
|
+
const v = verdaccioLogger;
|
|
446
|
+
const level = /* @__PURE__ */ __name((key) => {
|
|
447
|
+
const fn = v?.[key];
|
|
448
|
+
if (typeof fn === "function") {
|
|
449
|
+
return (msg, meta) => fn(meta ?? {}, `[xema-jwt] ${msg}`);
|
|
450
|
+
}
|
|
451
|
+
return (msg, meta) => {
|
|
452
|
+
const out = meta ? `[xema-jwt] ${msg} ${JSON.stringify(meta)}` : `[xema-jwt] ${msg}`;
|
|
453
|
+
if (key === "error") {
|
|
454
|
+
console.error(out);
|
|
455
|
+
} else if (key === "warn") {
|
|
456
|
+
console.warn(out);
|
|
457
|
+
} else {
|
|
458
|
+
console.log(out);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}, "level");
|
|
462
|
+
return {
|
|
463
|
+
info: level("info"),
|
|
464
|
+
warn: level("warn"),
|
|
465
|
+
error: level("error"),
|
|
466
|
+
debug: level("debug")
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
__name(adaptVerdaccioLogger, "adaptVerdaccioLogger");
|
|
470
|
+
function newCorrelationId() {
|
|
471
|
+
const { randomBytes } = require("node:crypto");
|
|
472
|
+
return randomBytes(6).toString("hex");
|
|
473
|
+
}
|
|
474
|
+
__name(newCorrelationId, "newCorrelationId");
|
|
475
|
+
|
|
476
|
+
// src/plugin.ts
|
|
477
|
+
var AUTHENTICATED_GROUP = "$authenticated";
|
|
478
|
+
function buildPlugin(config, deps = {}) {
|
|
479
|
+
const env = deps.env ?? process.env;
|
|
480
|
+
const logger = deps.logger ?? adaptVerdaccioLogger(void 0);
|
|
481
|
+
const verifier = deps.verifier ?? new JwtVerifier({
|
|
482
|
+
jwksUrl: requireEnv(config.keycloakRealmUrl ?? env.KEYCLOAK_REALM_URL, "KEYCLOAK_REALM_URL"),
|
|
483
|
+
...config.expectedIssuer !== void 0 ? { expectedIssuer: config.expectedIssuer } : env.KEYCLOAK_EXPECTED_ISSUER !== void 0 ? { expectedIssuer: env.KEYCLOAK_EXPECTED_ISSUER } : {},
|
|
484
|
+
...config.jwksRefreshMs !== void 0 ? { refreshMs: config.jwksRefreshMs } : {},
|
|
485
|
+
logger
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
authenticate(user, password, cb) {
|
|
489
|
+
const cid = newCorrelationId();
|
|
490
|
+
if (!password) {
|
|
491
|
+
logger.debug("authenticate skipped (empty password = anonymous read)", {
|
|
492
|
+
cid,
|
|
493
|
+
user
|
|
494
|
+
});
|
|
495
|
+
cb(null, false);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
void verifier.verify(password).then((claims) => {
|
|
499
|
+
logger.info("authenticate ok", {
|
|
500
|
+
cid,
|
|
501
|
+
sub: claims.sub,
|
|
502
|
+
orgId: claims.x_org_id,
|
|
503
|
+
role: claims.x_role
|
|
504
|
+
});
|
|
505
|
+
cb(null, [
|
|
506
|
+
AUTHENTICATED_GROUP,
|
|
507
|
+
`xema:role:${claims.x_role}`,
|
|
508
|
+
`xema:org:${claims.x_org_id}`,
|
|
509
|
+
`xema:sub:${claims.sub}`
|
|
510
|
+
]);
|
|
511
|
+
}).catch((err) => {
|
|
512
|
+
logger.warn("authenticate denied", {
|
|
513
|
+
cid,
|
|
514
|
+
user,
|
|
515
|
+
error: err.message
|
|
516
|
+
});
|
|
517
|
+
cb(null, false);
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
allow_access(remoteUser, pkg, cb) {
|
|
521
|
+
const cid = newCorrelationId();
|
|
522
|
+
const claims = claimsFromRemoteUser(remoteUser);
|
|
523
|
+
const decision = evaluateAcl({
|
|
524
|
+
packageName: pkg.name,
|
|
525
|
+
action: "access",
|
|
526
|
+
claims
|
|
527
|
+
});
|
|
528
|
+
if (decision.allow) {
|
|
529
|
+
logger.debug("allow_access ok", {
|
|
530
|
+
cid,
|
|
531
|
+
pkg: pkg.name,
|
|
532
|
+
orgId: claims?.x_org_id,
|
|
533
|
+
role: claims?.x_role
|
|
534
|
+
});
|
|
535
|
+
cb(null, true);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
logger.warn("allow_access denied", {
|
|
539
|
+
cid,
|
|
540
|
+
pkg: pkg.name,
|
|
541
|
+
reason: decision.reason,
|
|
542
|
+
message: decision.message,
|
|
543
|
+
orgId: claims?.x_org_id,
|
|
544
|
+
role: claims?.x_role
|
|
545
|
+
});
|
|
546
|
+
cb(buildDenyError(decision.reason, decision.message), false);
|
|
547
|
+
},
|
|
548
|
+
allow_publish(remoteUser, pkg, cb) {
|
|
549
|
+
const cid = newCorrelationId();
|
|
550
|
+
const claims = claimsFromRemoteUser(remoteUser);
|
|
551
|
+
const decision = evaluateAcl({
|
|
552
|
+
packageName: pkg.name,
|
|
553
|
+
action: "publish",
|
|
554
|
+
claims
|
|
555
|
+
});
|
|
556
|
+
if (decision.allow) {
|
|
557
|
+
logger.info("allow_publish ok", {
|
|
558
|
+
cid,
|
|
559
|
+
pkg: pkg.name,
|
|
560
|
+
orgId: claims?.x_org_id,
|
|
561
|
+
role: claims?.x_role,
|
|
562
|
+
sub: claims?.sub
|
|
563
|
+
});
|
|
564
|
+
cb(null, true);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
logger.warn("allow_publish denied", {
|
|
568
|
+
cid,
|
|
569
|
+
pkg: pkg.name,
|
|
570
|
+
reason: decision.reason,
|
|
571
|
+
message: decision.message,
|
|
572
|
+
orgId: claims?.x_org_id,
|
|
573
|
+
role: claims?.x_role
|
|
574
|
+
});
|
|
575
|
+
cb(buildDenyError(decision.reason, decision.message), false);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
__name(buildPlugin, "buildPlugin");
|
|
580
|
+
function requireEnv(value, name) {
|
|
581
|
+
if (!value) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`[xema-jwt] required env "${name}" is missing \u2014 plugin refuses to start (constitutional rule: no silent fallback to "auth disabled")`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
__name(requireEnv, "requireEnv");
|
|
589
|
+
function claimsFromRemoteUser(remoteUser) {
|
|
590
|
+
const groups = remoteUser.real_groups ?? remoteUser.groups ?? [];
|
|
591
|
+
if (!groups.includes(AUTHENTICATED_GROUP)) {
|
|
592
|
+
return void 0;
|
|
593
|
+
}
|
|
594
|
+
let role;
|
|
595
|
+
let orgId;
|
|
596
|
+
let sub;
|
|
597
|
+
for (const g of groups) {
|
|
598
|
+
if (g.startsWith("xema:role:")) {
|
|
599
|
+
const candidate = g.slice("xema:role:".length);
|
|
600
|
+
if (candidate === "platform" /* Platform */ || candidate === "org_admin" /* OrgAdmin */ || candidate === "org_member" /* OrgMember */ || candidate === "anonymous" /* Anonymous */) {
|
|
601
|
+
role = candidate;
|
|
602
|
+
}
|
|
603
|
+
} else if (g.startsWith("xema:org:")) {
|
|
604
|
+
orgId = g.slice("xema:org:".length);
|
|
605
|
+
} else if (g.startsWith("xema:sub:")) {
|
|
606
|
+
sub = g.slice("xema:sub:".length);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!role || !orgId || !sub) {
|
|
610
|
+
return void 0;
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
sub,
|
|
614
|
+
exp: Number.MAX_SAFE_INTEGER,
|
|
615
|
+
x_org_id: orgId,
|
|
616
|
+
x_role: role
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
__name(claimsFromRemoteUser, "claimsFromRemoteUser");
|
|
620
|
+
function buildDenyError(reason, message) {
|
|
621
|
+
const err = new Error(message);
|
|
622
|
+
err.code = "XEMA_ACL_DENIED";
|
|
623
|
+
err.reason = reason;
|
|
624
|
+
return err;
|
|
625
|
+
}
|
|
626
|
+
__name(buildDenyError, "buildDenyError");
|
|
627
|
+
function pluginFactory(config, stuff = {}) {
|
|
628
|
+
const logger = adaptVerdaccioLogger(stuff.logger);
|
|
629
|
+
return buildPlugin(config, { logger });
|
|
630
|
+
}
|
|
631
|
+
__name(pluginFactory, "pluginFactory");
|
|
632
|
+
var plugin_default = pluginFactory;
|
|
633
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
634
|
+
0 && (module.exports = {
|
|
635
|
+
AclDenyReason,
|
|
636
|
+
JwtVerifier,
|
|
637
|
+
PackageTier,
|
|
638
|
+
XemaRegistryRole,
|
|
639
|
+
__buildBiomeForTests,
|
|
640
|
+
buildPlugin,
|
|
641
|
+
classifyPackage,
|
|
642
|
+
evaluateAcl,
|
|
643
|
+
packageTierToVisibility,
|
|
644
|
+
pluginFactory
|
|
645
|
+
});
|
|
646
|
+
module.exports = module.exports.default;
|