@zauso-ai/capstan-auth 0.2.0 → 0.3.1

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 (51) hide show
  1. package/dist/dpop.d.ts +46 -0
  2. package/dist/dpop.d.ts.map +1 -0
  3. package/dist/dpop.js +259 -0
  4. package/dist/dpop.js.map +1 -0
  5. package/dist/execution.d.ts +10 -0
  6. package/dist/execution.d.ts.map +1 -0
  7. package/dist/execution.js +50 -0
  8. package/dist/execution.js.map +1 -0
  9. package/dist/harness-authorizer.d.ts +10 -0
  10. package/dist/harness-authorizer.d.ts.map +1 -0
  11. package/dist/harness-authorizer.js +90 -0
  12. package/dist/harness-authorizer.js.map +1 -0
  13. package/dist/index.d.ts +14 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/middleware.d.ts +6 -3
  18. package/dist/middleware.d.ts.map +1 -1
  19. package/dist/middleware.js +209 -30
  20. package/dist/middleware.js.map +1 -1
  21. package/dist/oauth.d.ts +47 -0
  22. package/dist/oauth.d.ts.map +1 -0
  23. package/dist/oauth.js +199 -0
  24. package/dist/oauth.js.map +1 -0
  25. package/dist/permissions.d.ts +12 -22
  26. package/dist/permissions.d.ts.map +1 -1
  27. package/dist/permissions.js +91 -33
  28. package/dist/permissions.js.map +1 -1
  29. package/dist/runtime-authorizer.d.ts +28 -0
  30. package/dist/runtime-authorizer.d.ts.map +1 -0
  31. package/dist/runtime-authorizer.js +136 -0
  32. package/dist/runtime-authorizer.js.map +1 -0
  33. package/dist/runtime-grants.d.ts +31 -0
  34. package/dist/runtime-grants.d.ts.map +1 -0
  35. package/dist/runtime-grants.js +96 -0
  36. package/dist/runtime-grants.js.map +1 -0
  37. package/dist/session.d.ts +3 -3
  38. package/dist/session.d.ts.map +1 -1
  39. package/dist/session.js +21 -3
  40. package/dist/session.js.map +1 -1
  41. package/dist/store.d.ts +27 -0
  42. package/dist/store.d.ts.map +1 -0
  43. package/dist/store.js +46 -0
  44. package/dist/store.js.map +1 -0
  45. package/dist/types.d.ts +109 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/workload.d.ts +46 -0
  48. package/dist/workload.d.ts.map +1 -0
  49. package/dist/workload.js +227 -0
  50. package/dist/workload.js.map +1 -0
  51. package/package.json +3 -2
package/dist/types.d.ts CHANGED
@@ -1,40 +1,148 @@
1
+ export type AuthContextType = "human" | "agent" | "anonymous" | "workload";
2
+ export type ActorKind = "user" | "agent" | "workload" | "system" | "anonymous";
3
+ export type CredentialKind = "session" | "oauth" | "api_key" | "mtls" | "dpop" | "run_token" | "approval_token" | "anonymous";
4
+ export type ExecutionKind = "request" | "run" | "tool_call" | "approval" | "schedule" | "release" | "mcp_invocation";
5
+ export interface AuthCookieConfig {
6
+ path?: string;
7
+ domain?: string;
8
+ secure?: boolean;
9
+ httpOnly?: boolean;
10
+ sameSite?: "Strict" | "Lax" | "None";
11
+ }
12
+ export interface AuthGrant {
13
+ resource: string;
14
+ action: string;
15
+ scope?: Record<string, string>;
16
+ effect?: "allow" | "deny";
17
+ expiresAt?: string;
18
+ constraints?: Record<string, unknown>;
19
+ }
20
+ export interface AuthGrantRequirement {
21
+ resource: string;
22
+ action: string;
23
+ scope?: Record<string, string>;
24
+ }
25
+ export interface ActorIdentity {
26
+ kind: ActorKind;
27
+ id: string;
28
+ displayName?: string;
29
+ role?: string;
30
+ email?: string;
31
+ claims?: Record<string, unknown>;
32
+ }
33
+ export interface CredentialProof {
34
+ kind: CredentialKind;
35
+ subjectId: string;
36
+ presentedAt: string;
37
+ expiresAt?: string;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+ export interface ExecutionIdentity {
41
+ kind: ExecutionKind;
42
+ id: string;
43
+ parentId?: string;
44
+ metadata?: Record<string, unknown>;
45
+ }
46
+ export interface DelegationTargetRef {
47
+ kind: ActorKind | ExecutionKind;
48
+ id: string;
49
+ }
50
+ export interface DelegationLink {
51
+ from: DelegationTargetRef;
52
+ to: DelegationTargetRef;
53
+ reason: string;
54
+ issuedAt: string;
55
+ metadata?: Record<string, unknown>;
56
+ }
57
+ export interface AuthEnvelope {
58
+ actor: ActorIdentity;
59
+ credential: CredentialProof;
60
+ execution?: ExecutionIdentity;
61
+ delegation: DelegationLink[];
62
+ grants: AuthGrant[];
63
+ }
1
64
  export interface AuthConfig {
2
65
  session: {
3
66
  secret: string;
4
67
  maxAge?: string;
68
+ issuer?: string;
69
+ audience?: string;
70
+ cookieName?: string;
71
+ cookie?: AuthCookieConfig;
5
72
  };
6
73
  apiKeys?: {
7
74
  prefix?: string;
8
75
  headerName?: string;
9
76
  };
77
+ /** Trusted SPIFFE trust domains for mTLS workload authentication. */
78
+ trustedDomains?: string[];
79
+ /** Whether to require client certificates (mTLS). */
80
+ mtls?: boolean;
81
+ /** OAuth 2.0 provider configuration for Google, GitHub, etc. */
82
+ oauth?: import("./oauth.js").OAuthConfig;
10
83
  }
11
84
  export interface SessionPayload {
12
85
  userId: string;
13
86
  email?: string;
14
87
  role?: string;
88
+ displayName?: string;
89
+ permissions?: string[];
90
+ claims?: Record<string, unknown>;
91
+ sessionId?: string;
92
+ iss?: string;
93
+ aud?: string | string[];
15
94
  iat: number;
16
95
  exp: number;
17
96
  }
97
+ export interface SessionSigningOptions {
98
+ maxAge?: string;
99
+ issuer?: string;
100
+ audience?: string | string[];
101
+ }
102
+ export interface SessionVerificationOptions {
103
+ issuer?: string;
104
+ audience?: string;
105
+ }
18
106
  export interface AgentCredential {
19
107
  id: string;
20
108
  name: string;
21
109
  apiKeyHash: string;
22
110
  apiKeyPrefix: string;
23
111
  permissions: string[];
112
+ grants?: AuthGrant[];
113
+ claims?: Record<string, unknown>;
24
114
  revokedAt?: string;
25
115
  }
26
116
  export interface AuthContext {
27
117
  isAuthenticated: boolean;
28
- type: "human" | "agent" | "anonymous";
118
+ type: AuthContextType;
119
+ actor: ActorIdentity;
120
+ credential: CredentialProof;
121
+ execution?: ExecutionIdentity;
122
+ delegation: DelegationLink[];
123
+ grants: AuthGrant[];
124
+ envelope?: AuthEnvelope;
29
125
  userId?: string;
30
126
  role?: string;
31
127
  email?: string;
32
128
  agentId?: string;
33
129
  agentName?: string;
34
130
  permissions?: string[];
131
+ /** DPoP key thumbprint — present when the request included a valid DPoP proof. */
132
+ dpopThumbprint?: string;
133
+ /** SPIFFE ID from client certificate (e.g. "spiffe://example.org/agent/crawler"). */
134
+ spiffeId?: string;
135
+ /** Client certificate fingerprint (SHA-256 hex digest). */
136
+ certFingerprint?: string;
35
137
  }
36
138
  export interface AuthResolverDeps {
37
139
  /** Look up an agent credential by API key prefix */
38
140
  findAgentByKeyPrefix?: (prefix: string) => Promise<AgentCredential | null>;
141
+ /** Resolve extra grants after credential verification. */
142
+ resolveAdditionalGrants?: (auth: AuthContext, request: Request) => Promise<AuthGrant[] | string[] | undefined>;
143
+ /** Attach richer execution identity to the resolved auth envelope. */
144
+ resolveExecution?: (auth: AuthContext, request: Request) => Promise<ExecutionIdentity | undefined>;
145
+ /** Attach delegation provenance for runtime / harness flows. */
146
+ resolveDelegation?: (auth: AuthContext, request: Request) => Promise<DelegationLink[] | undefined>;
39
147
  }
40
148
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,oBAAoB,CAAC,EAAE,CACrB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parsed workload identity extracted from a client certificate.
3
+ */
4
+ export interface WorkloadIdentity {
5
+ /** Full SPIFFE ID URI (e.g. "spiffe://example.org/agent/crawler"). */
6
+ spiffeId: string;
7
+ /** Trust domain portion of the SPIFFE ID (e.g. "example.org"). */
8
+ trustDomain: string;
9
+ /** Workload path portion of the SPIFFE ID (e.g. "/agent/crawler"). */
10
+ workloadPath: string;
11
+ /** SHA-256 hex digest of the PEM-encoded client certificate. */
12
+ certFingerprint: string;
13
+ }
14
+ /**
15
+ * Validate that a string is a well-formed SPIFFE ID per the SPIFFE spec:
16
+ *
17
+ * - Scheme must be `spiffe`
18
+ * - Trust domain must be a non-empty hostname-like string (lower-case
19
+ * alphanumerics, hyphens, dots; no leading/trailing hyphens or dots).
20
+ * - Workload path must be non-empty and start with `/`.
21
+ */
22
+ export declare function isValidSpiffeId(id: string): boolean;
23
+ /**
24
+ * Extract a workload identity from client certificate information provided
25
+ * via request headers.
26
+ *
27
+ * In a typical deployment the mTLS termination happens at a reverse proxy
28
+ * (Envoy, Nginx, Istio ingress, etc.) which forwards the client certificate
29
+ * and its SPIFFE ID through HTTP headers.
30
+ *
31
+ * Supported header combinations:
32
+ *
33
+ * | SPIFFE ID source | Cert source |
34
+ * |---------------------------------------|--------------------------|
35
+ * | `X-Client-Cert-Spiffe-Id` header | `X-Client-Cert` |
36
+ * | `URI=` in `X-Forwarded-Client-Cert` | `X-Forwarded-Client-Cert`|
37
+ *
38
+ * @param certOrHeaders PEM-encoded client certificate string, or a
39
+ * header map (e.g. from `Object.fromEntries(request.headers)`).
40
+ * @param trustedDomains Whitelist of SPIFFE trust domains to accept.
41
+ * @returns The parsed `WorkloadIdentity` if the certificate is present, the
42
+ * SPIFFE ID is valid, and the trust domain is in the whitelist.
43
+ * Returns `null` otherwise.
44
+ */
45
+ export declare function extractWorkloadIdentity(certOrHeaders: string | Record<string, string | undefined>, trustedDomains: string[]): WorkloadIdentity | null;
46
+ //# sourceMappingURL=workload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload.d.ts","sourceRoot":"","sources":["../src/workload.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;CACzB;AAiBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CA6BnD;AAkID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC1D,cAAc,EAAE,MAAM,EAAE,GACvB,gBAAgB,GAAG,IAAI,CAyCzB"}
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Headers that reverse proxies commonly use to forward client certificates.
4
+ *
5
+ * - `x-client-cert` — raw PEM or URL-encoded PEM (Envoy, Nginx, etc.)
6
+ * - `x-forwarded-client-cert` — Envoy XFCC header (may contain key=value pairs)
7
+ */
8
+ const CLIENT_CERT_HEADERS = [
9
+ "x-client-cert",
10
+ "x-forwarded-client-cert",
11
+ ];
12
+ // ---------------------------------------------------------------------------
13
+ // SPIFFE ID validation
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Validate that a string is a well-formed SPIFFE ID per the SPIFFE spec:
17
+ *
18
+ * - Scheme must be `spiffe`
19
+ * - Trust domain must be a non-empty hostname-like string (lower-case
20
+ * alphanumerics, hyphens, dots; no leading/trailing hyphens or dots).
21
+ * - Workload path must be non-empty and start with `/`.
22
+ */
23
+ export function isValidSpiffeId(id) {
24
+ if (typeof id !== "string" || id.length === 0)
25
+ return false;
26
+ // Must start with the spiffe scheme.
27
+ if (!id.startsWith("spiffe://"))
28
+ return false;
29
+ const rest = id.slice("spiffe://".length);
30
+ // Find the first "/" after the trust domain.
31
+ const slashIndex = rest.indexOf("/");
32
+ if (slashIndex <= 0)
33
+ return false; // no trust domain or no path
34
+ const trustDomain = rest.slice(0, slashIndex);
35
+ const workloadPath = rest.slice(slashIndex);
36
+ // Trust domain: valid DNS-like label(s), no leading/trailing dot or hyphen.
37
+ if (!isValidTrustDomain(trustDomain))
38
+ return false;
39
+ // Workload path must be non-empty (beyond the leading slash).
40
+ if (workloadPath.length < 2)
41
+ return false;
42
+ // Path segments must not be empty (no double slashes) and must not
43
+ // contain `.` or `..` traversals.
44
+ const segments = workloadPath.slice(1).split("/");
45
+ for (const seg of segments) {
46
+ if (seg.length === 0 || seg === "." || seg === "..")
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ /**
52
+ * A trust domain is a valid lower-case DNS name: labels separated by dots,
53
+ * each label consisting of alphanumerics and hyphens, not starting/ending
54
+ * with a hyphen.
55
+ */
56
+ function isValidTrustDomain(domain) {
57
+ if (domain.length === 0 || domain.length > 255)
58
+ return false;
59
+ const labels = domain.split(".");
60
+ for (const label of labels) {
61
+ if (label.length === 0 || label.length > 63)
62
+ return false;
63
+ if (/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(label) === false)
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Certificate parsing helpers
70
+ // ---------------------------------------------------------------------------
71
+ /**
72
+ * Extract the PEM-encoded client certificate from request headers.
73
+ *
74
+ * Supports:
75
+ * - `X-Client-Cert: <PEM or URL-encoded PEM>`
76
+ * - `X-Forwarded-Client-Cert: Cert="<URL-encoded PEM>"` (Envoy XFCC format)
77
+ */
78
+ function extractPemFromHeaders(headers) {
79
+ // Normalise header names to lower case for case-insensitive lookup.
80
+ const normalised = {};
81
+ for (const [k, v] of Object.entries(headers)) {
82
+ if (v !== undefined) {
83
+ normalised[k.toLowerCase()] = v;
84
+ }
85
+ }
86
+ for (const headerName of CLIENT_CERT_HEADERS) {
87
+ const raw = normalised[headerName];
88
+ if (!raw || raw.length === 0)
89
+ continue;
90
+ // XFCC header from Envoy may look like:
91
+ // By=...;Cert="<url-encoded PEM>";Hash=...
92
+ // Extract the Cert value if present.
93
+ const certMatch = raw.match(/Cert="([^"]+)"/i);
94
+ if (certMatch?.[1]) {
95
+ return decodePem(certMatch[1]);
96
+ }
97
+ // Otherwise treat the entire header value as PEM (possibly URL-encoded).
98
+ return decodePem(raw);
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Decode a PEM string that may be URL-encoded (common in proxy headers).
104
+ */
105
+ function decodePem(value) {
106
+ // If the value contains %2F or %2B it was URL-encoded.
107
+ if (value.includes("%")) {
108
+ try {
109
+ return decodeURIComponent(value);
110
+ }
111
+ catch {
112
+ // Not valid URL encoding — use as-is.
113
+ }
114
+ }
115
+ return value;
116
+ }
117
+ /**
118
+ * Compute the SHA-256 hex fingerprint of a PEM-encoded certificate.
119
+ *
120
+ * The fingerprint is computed over the raw PEM text (including header/footer
121
+ * lines) to keep the implementation dependency-free. This is sufficient for
122
+ * identity-binding purposes as each unique cert will produce a unique hash.
123
+ */
124
+ function computeCertFingerprint(pem) {
125
+ return createHash("sha256").update(pem).digest("hex");
126
+ }
127
+ /**
128
+ * Extract the SPIFFE URI SAN from a PEM-encoded certificate.
129
+ *
130
+ * Real X.509 parsing requires ASN.1 decoding which would need a dependency.
131
+ * Instead we use a pragmatic approach: the SPIFFE ID is looked up from a
132
+ * companion header (`X-Client-Cert-Spiffe-Id`) that mTLS-terminating proxies
133
+ * (Envoy, Istio, Linkerd) can be configured to set, or it can be embedded
134
+ * in the XFCC header as `URI=spiffe://...`.
135
+ *
136
+ * If neither is available we fall back to scanning the PEM base64 payload
137
+ * for the `spiffe://` URI (works for unencrypted certs where the SAN is
138
+ * visible in the base64 text — a useful heuristic but not a substitute for
139
+ * real ASN.1 parsing).
140
+ */
141
+ function extractSpiffeIdFromHeaders(headers) {
142
+ const normalised = {};
143
+ for (const [k, v] of Object.entries(headers)) {
144
+ if (v !== undefined) {
145
+ normalised[k.toLowerCase()] = v;
146
+ }
147
+ }
148
+ // 1. Explicit header set by the proxy.
149
+ const explicit = normalised["x-client-cert-spiffe-id"];
150
+ if (explicit && isValidSpiffeId(explicit)) {
151
+ return explicit;
152
+ }
153
+ // 2. XFCC URI field (Envoy sets `URI=spiffe://...`).
154
+ const xfcc = normalised["x-forwarded-client-cert"];
155
+ if (xfcc) {
156
+ const uriMatch = xfcc.match(/URI=(spiffe:\/\/[^;,"]+)/i);
157
+ if (uriMatch?.[1] && isValidSpiffeId(uriMatch[1])) {
158
+ return uriMatch[1];
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Public API
165
+ // ---------------------------------------------------------------------------
166
+ /**
167
+ * Extract a workload identity from client certificate information provided
168
+ * via request headers.
169
+ *
170
+ * In a typical deployment the mTLS termination happens at a reverse proxy
171
+ * (Envoy, Nginx, Istio ingress, etc.) which forwards the client certificate
172
+ * and its SPIFFE ID through HTTP headers.
173
+ *
174
+ * Supported header combinations:
175
+ *
176
+ * | SPIFFE ID source | Cert source |
177
+ * |---------------------------------------|--------------------------|
178
+ * | `X-Client-Cert-Spiffe-Id` header | `X-Client-Cert` |
179
+ * | `URI=` in `X-Forwarded-Client-Cert` | `X-Forwarded-Client-Cert`|
180
+ *
181
+ * @param certOrHeaders PEM-encoded client certificate string, or a
182
+ * header map (e.g. from `Object.fromEntries(request.headers)`).
183
+ * @param trustedDomains Whitelist of SPIFFE trust domains to accept.
184
+ * @returns The parsed `WorkloadIdentity` if the certificate is present, the
185
+ * SPIFFE ID is valid, and the trust domain is in the whitelist.
186
+ * Returns `null` otherwise.
187
+ */
188
+ export function extractWorkloadIdentity(certOrHeaders, trustedDomains) {
189
+ if (trustedDomains.length === 0)
190
+ return null;
191
+ let pem;
192
+ let spiffeId;
193
+ if (typeof certOrHeaders === "string") {
194
+ // Raw PEM string provided directly — cannot extract SPIFFE ID from
195
+ // headers, so this path is only useful if the caller also provides
196
+ // the SPIFFE ID separately. For header-based flows use the object form.
197
+ pem = certOrHeaders.length > 0 ? certOrHeaders : null;
198
+ spiffeId = null;
199
+ }
200
+ else {
201
+ pem = extractPemFromHeaders(certOrHeaders);
202
+ spiffeId = extractSpiffeIdFromHeaders(certOrHeaders);
203
+ }
204
+ // We need both a certificate (for fingerprinting) and a SPIFFE ID.
205
+ if (!pem || !spiffeId)
206
+ return null;
207
+ if (!isValidSpiffeId(spiffeId))
208
+ return null;
209
+ // Parse the trust domain from the SPIFFE ID.
210
+ const rest = spiffeId.slice("spiffe://".length);
211
+ const slashIndex = rest.indexOf("/");
212
+ if (slashIndex <= 0)
213
+ return null;
214
+ const trustDomain = rest.slice(0, slashIndex);
215
+ const workloadPath = rest.slice(slashIndex);
216
+ // Verify the trust domain is in the whitelist.
217
+ if (!trustedDomains.includes(trustDomain))
218
+ return null;
219
+ const certFingerprint = computeCertFingerprint(pem);
220
+ return {
221
+ spiffeId,
222
+ trustDomain,
223
+ workloadPath,
224
+ certFingerprint,
225
+ };
226
+ }
227
+ //# sourceMappingURL=workload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload.js","sourceRoot":"","sources":["../src/workload.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauso-ai/capstan-auth",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
- "typecheck": "tsc -p tsconfig.json --noEmit"
15
+ "typecheck": "tsc -p tsconfig.json --noEmit",
16
+ "clean": "rm -rf dist"
16
17
  },
17
18
  "description": "Authentication for Capstan — JWT sessions, API key auth for AI agents, permissions",
18
19
  "files": [