@rakomi/node 0.0.0 → 0.1.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -1
  3. package/SECURITY.md +206 -0
  4. package/dist/agents.d.ts +90 -0
  5. package/dist/agents.js +203 -0
  6. package/dist/anonymous.d.ts +50 -0
  7. package/dist/anonymous.js +105 -0
  8. package/dist/ciba.d.ts +97 -0
  9. package/dist/ciba.js +282 -0
  10. package/dist/client.d.ts +93 -0
  11. package/dist/client.js +202 -0
  12. package/dist/credentials.d.ts +87 -0
  13. package/dist/credentials.js +104 -0
  14. package/dist/device.d.ts +76 -0
  15. package/dist/device.js +244 -0
  16. package/dist/doctor.d.ts +11 -0
  17. package/dist/doctor.js +135 -0
  18. package/dist/dpop-session.d.ts +90 -0
  19. package/dist/dpop-session.js +127 -0
  20. package/dist/dpop.d.ts +24 -0
  21. package/dist/dpop.js +51 -0
  22. package/dist/env-detect.d.ts +11 -0
  23. package/dist/env-detect.js +26 -0
  24. package/dist/errors.d.ts +307 -0
  25. package/dist/errors.js +385 -0
  26. package/dist/eudi.d.ts +23 -0
  27. package/dist/eudi.js +27 -0
  28. package/dist/flags.d.ts +50 -0
  29. package/dist/flags.js +173 -0
  30. package/dist/guards.d.ts +16 -0
  31. package/dist/guards.js +104 -0
  32. package/dist/index.d.ts +30 -0
  33. package/dist/index.js +18 -0
  34. package/dist/internal/canonical-url.d.ts +13 -0
  35. package/dist/internal/canonical-url.js +52 -0
  36. package/dist/internal/shared-constants.d.ts +3 -0
  37. package/dist/internal/shared-constants.js +3 -0
  38. package/dist/jwks-cache.d.ts +31 -0
  39. package/dist/jwks-cache.js +135 -0
  40. package/dist/link.d.ts +73 -0
  41. package/dist/link.js +262 -0
  42. package/dist/middleware.d.ts +21 -0
  43. package/dist/middleware.js +84 -0
  44. package/dist/oauth.d.ts +46 -0
  45. package/dist/oauth.js +457 -0
  46. package/dist/rbac.d.ts +12 -0
  47. package/dist/rbac.js +20 -0
  48. package/dist/token-exchange.d.ts +65 -0
  49. package/dist/token-exchange.js +163 -0
  50. package/dist/types.d.ts +436 -0
  51. package/dist/types.js +1 -0
  52. package/dist/verify-publisher-webhook.d.ts +25 -0
  53. package/dist/verify-publisher-webhook.js +47 -0
  54. package/dist/verify-token.d.ts +3 -0
  55. package/dist/verify-token.js +148 -0
  56. package/dist/verify-webhook.d.ts +7 -0
  57. package/dist/verify-webhook.js +101 -0
  58. package/package.json +61 -5
  59. package/sbom.cdx.json +52 -0
package/dist/flags.js ADDED
@@ -0,0 +1,173 @@
1
+ const MAX_CACHE_ENTRIES = 1000;
2
+ const ETAG_REGEX = /^"[^"]*"$/;
3
+ class CircuitBreaker {
4
+ state = 'closed';
5
+ failures = 0;
6
+ threshold = 3;
7
+ resetMs = 60_000;
8
+ resetTimer = null;
9
+ get status() {
10
+ return this.state;
11
+ }
12
+ recordFailure() {
13
+ if (this.state === 'half-open') {
14
+ this.state = 'open';
15
+ this.failures = 0;
16
+ this.resetTimer = setTimeout(() => {
17
+ this.state = 'half-open';
18
+ this.failures = 0;
19
+ this.resetTimer = null;
20
+ }, this.resetMs);
21
+ return;
22
+ }
23
+ this.failures++;
24
+ if (this.failures >= this.threshold && this.state === 'closed') {
25
+ this.state = 'open';
26
+ this.resetTimer = setTimeout(() => {
27
+ this.state = 'half-open';
28
+ this.failures = 0;
29
+ this.resetTimer = null;
30
+ }, this.resetMs);
31
+ }
32
+ }
33
+ recordSuccess() {
34
+ if (this.state === 'half-open') {
35
+ this.state = 'closed';
36
+ this.failures = 0;
37
+ if (this.resetTimer !== null) {
38
+ clearTimeout(this.resetTimer);
39
+ this.resetTimer = null;
40
+ }
41
+ }
42
+ }
43
+ isOpen() {
44
+ return this.state === 'open';
45
+ }
46
+ }
47
+ export class FlagsClient {
48
+ baseUrl;
49
+ apiKey;
50
+ cache = new Map();
51
+ circuit = new CircuitBreaker();
52
+ constructor(baseUrl, apiKey) {
53
+ this.baseUrl = baseUrl;
54
+ this.apiKey = apiKey;
55
+ }
56
+ get circuitStatus() {
57
+ return this.circuit.status;
58
+ }
59
+ buildCacheKey(ctx) {
60
+ const userId = ctx?.userId ?? '__anon__';
61
+ const sortedKeys = ctx?.keys ? [...ctx.keys].sort().join(',') : '__all__';
62
+ return `${this.apiKey}:${userId}:${sortedKeys}`;
63
+ }
64
+ isCacheValid(entry) {
65
+ return Date.now() - entry.fetchedAt < entry.ttl * 1000;
66
+ }
67
+ setCacheEntry(key, entry) {
68
+ if (this.cache.size >= MAX_CACHE_ENTRIES && !this.cache.has(key)) {
69
+ const oldest = this.cache.keys().next().value;
70
+ if (oldest !== undefined) {
71
+ this.cache.delete(oldest);
72
+ }
73
+ }
74
+ this.cache.set(key, entry);
75
+ }
76
+ async fetchFlags(ctx, _opts) {
77
+ const body = {
78
+ ...(ctx?.userId ? { user_id: ctx.userId } : {}),
79
+ ...(ctx?.userMetadata ? { user_metadata: ctx.userMetadata } : {}),
80
+ ...(ctx?.keys?.length ? { keys: ctx.keys } : {}),
81
+ };
82
+ const cacheKey = this.buildCacheKey(ctx);
83
+ const cached = this.cache.get(cacheKey);
84
+ const etag = cached?.etag ?? null;
85
+ const headers = {
86
+ 'Authorization': `Bearer ${this.apiKey}`,
87
+ 'Content-Type': 'application/json',
88
+ };
89
+ if (etag) {
90
+ headers['If-None-Match'] = etag;
91
+ }
92
+ const response = await fetch(`${this.baseUrl}/v1/flags/evaluate`, {
93
+ method: 'POST',
94
+ headers,
95
+ body: JSON.stringify(body),
96
+ redirect: 'error',
97
+ signal: AbortSignal.timeout(10_000),
98
+ });
99
+ if (response.status === 304 && cached) {
100
+ return { flags: cached.flags, etag: cached.etag };
101
+ }
102
+ if (!response.ok) {
103
+ const err = new (class extends Error {
104
+ })(`HTTP ${response.status}`);
105
+ throw err;
106
+ }
107
+ const data = (await response.json());
108
+ const newEtag = response.headers.get('etag');
109
+ const validEtag = newEtag && ETAG_REGEX.test(newEtag) ? newEtag : null;
110
+ return { flags: data.flags ?? {}, etag: validEtag };
111
+ }
112
+ async get(key, ctx, opts) {
113
+ try {
114
+ if (this.circuit.isOpen()) {
115
+ return { ok: false, error: { code: 'CIRCUIT_OPEN', message: 'Circuit breaker open — evaluate endpoint unavailable' } };
116
+ }
117
+ const effectiveTtl = Math.max(opts?.ttl ?? 60, 10);
118
+ const cacheKey = this.buildCacheKey(ctx);
119
+ const cached = this.cache.get(cacheKey);
120
+ if (!opts?.skipCache && cached && this.isCacheValid(cached)) {
121
+ const value = cached.flags[key];
122
+ return { ok: true, value: value };
123
+ }
124
+ const { flags, etag } = await this.fetchFlags(ctx, opts);
125
+ this.setCacheEntry(cacheKey, { flags, etag, fetchedAt: Date.now(), ttl: effectiveTtl });
126
+ this.circuit.recordSuccess();
127
+ const value = flags[key];
128
+ return { ok: true, value: value };
129
+ }
130
+ catch (err) {
131
+ this.circuit.recordFailure();
132
+ const message = err instanceof Error ? err.message : 'Unknown error';
133
+ return { ok: false, error: { code: 'FETCH_FAILED', message } };
134
+ }
135
+ }
136
+ async getAll(ctx, opts) {
137
+ try {
138
+ if (this.circuit.isOpen()) {
139
+ return { ok: false, error: { code: 'CIRCUIT_OPEN', message: 'Circuit breaker open — evaluate endpoint unavailable' } };
140
+ }
141
+ const effectiveTtl = Math.max(opts?.ttl ?? 60, 10);
142
+ const cacheKey = this.buildCacheKey(ctx);
143
+ const cached = this.cache.get(cacheKey);
144
+ if (!opts?.skipCache && cached && this.isCacheValid(cached)) {
145
+ return { ok: true, flags: cached.flags };
146
+ }
147
+ const { flags, etag } = await this.fetchFlags(ctx, opts);
148
+ this.setCacheEntry(cacheKey, { flags, etag, fetchedAt: Date.now(), ttl: effectiveTtl });
149
+ this.circuit.recordSuccess();
150
+ return { ok: true, flags };
151
+ }
152
+ catch (err) {
153
+ this.circuit.recordFailure();
154
+ const message = err instanceof Error ? err.message : 'Unknown error';
155
+ return { ok: false, error: { code: 'FETCH_FAILED', message } };
156
+ }
157
+ }
158
+ startPolling(ctx, opts) {
159
+ const effectiveTtl = Math.max(opts?.ttl ?? 60, 10);
160
+ const intervalId = setInterval(() => {
161
+ void this.getAll(ctx, { ...opts, ttl: effectiveTtl, skipCache: true });
162
+ }, effectiveTtl * 1000);
163
+ return { stop: () => clearInterval(intervalId) };
164
+ }
165
+ flush(cacheKey) {
166
+ if (cacheKey !== undefined) {
167
+ this.cache.delete(cacheKey);
168
+ }
169
+ else {
170
+ this.cache.clear();
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,16 @@
1
+ import type { MiddlewareRequest, MiddlewareResponse, NextFunction } from './middleware.js';
2
+ import type { SdkEnvironment, TokenPayload } from './types.js';
3
+ type AuthenticatedRequest = MiddlewareRequest & {
4
+ auth?: TokenPayload;
5
+ };
6
+ /**
7
+ * Express-compatible middleware guard. For Hono/Fastify, use hasPermission() directly.
8
+ * Checks JWT claims (offline verification) — NOT a live API call.
9
+ */
10
+ export declare function requirePermission(permission: string, environmentOverride?: SdkEnvironment): (req: AuthenticatedRequest, res: MiddlewareResponse, next: NextFunction) => void;
11
+ /**
12
+ * Express-compatible middleware guard. For Hono/Fastify, use hasRole() directly.
13
+ * Checks JWT claims (offline verification) — NOT a live API call.
14
+ */
15
+ export declare function requireRole(roleKey: string, environmentOverride?: SdkEnvironment): (req: AuthenticatedRequest, res: MiddlewareResponse, next: NextFunction) => void;
16
+ export {};
package/dist/guards.js ADDED
@@ -0,0 +1,104 @@
1
+ import { resolveVerbose } from './middleware.js';
2
+ import { hasPermission, hasRole } from './rbac.js';
3
+ /**
4
+ * Express-compatible middleware guard. For Hono/Fastify, use hasPermission() directly.
5
+ * Checks JWT claims (offline verification) — NOT a live API call.
6
+ */
7
+ export function requirePermission(permission, environmentOverride) {
8
+ return (req, res, next) => {
9
+ try {
10
+ const verbose = resolveVerbose(req, environmentOverride);
11
+ if (!req.auth) {
12
+ res.setHeader('WWW-Authenticate', 'Bearer realm="rakomi"');
13
+ res.status(401).json({
14
+ error: {
15
+ code: 'auth/not_authenticated',
16
+ message: 'Authentication required',
17
+ ...(verbose && { suggestion: 'Ensure rakomi.middleware() is applied before requirePermission()' }),
18
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#not-authenticated',
19
+ },
20
+ });
21
+ return;
22
+ }
23
+ const payload = {
24
+ ...req.auth,
25
+ permissions: req.auth.permissions ?? [],
26
+ roles: req.auth.roles ?? [],
27
+ };
28
+ if (!hasPermission(payload, permission)) {
29
+ res.setHeader('WWW-Authenticate', verbose
30
+ ? `Bearer realm="rakomi", error="insufficient_scope", scope="${permission}"`
31
+ : 'Bearer realm="rakomi", error="insufficient_scope"');
32
+ res.status(403).json({
33
+ error: {
34
+ code: 'auth/insufficient_permissions',
35
+ message: verbose ? `Missing permission: ${permission}` : 'Insufficient permissions',
36
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#insufficient-permissions',
37
+ },
38
+ });
39
+ return;
40
+ }
41
+ next();
42
+ }
43
+ catch {
44
+ res.setHeader('WWW-Authenticate', 'Bearer realm="rakomi", error="invalid_token"');
45
+ res.status(401).json({
46
+ error: {
47
+ code: 'auth/guard_error',
48
+ message: 'Authorization check failed',
49
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#guard-error',
50
+ },
51
+ });
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Express-compatible middleware guard. For Hono/Fastify, use hasRole() directly.
57
+ * Checks JWT claims (offline verification) — NOT a live API call.
58
+ */
59
+ export function requireRole(roleKey, environmentOverride) {
60
+ return (req, res, next) => {
61
+ try {
62
+ const verbose = resolveVerbose(req, environmentOverride);
63
+ if (!req.auth) {
64
+ res.setHeader('WWW-Authenticate', 'Bearer realm="rakomi"');
65
+ res.status(401).json({
66
+ error: {
67
+ code: 'auth/not_authenticated',
68
+ message: 'Authentication required',
69
+ ...(verbose && { suggestion: 'Ensure rakomi.middleware() is applied before requireRole()' }),
70
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#not-authenticated',
71
+ },
72
+ });
73
+ return;
74
+ }
75
+ const payload = {
76
+ ...req.auth,
77
+ permissions: req.auth.permissions ?? [],
78
+ roles: req.auth.roles ?? [],
79
+ };
80
+ if (!hasRole(payload, roleKey)) {
81
+ res.setHeader('WWW-Authenticate', 'Bearer realm="rakomi", error="insufficient_scope"');
82
+ res.status(403).json({
83
+ error: {
84
+ code: 'auth/insufficient_role',
85
+ message: verbose ? `Missing role: ${roleKey}` : 'Insufficient role',
86
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#insufficient-role',
87
+ },
88
+ });
89
+ return;
90
+ }
91
+ next();
92
+ }
93
+ catch {
94
+ res.setHeader('WWW-Authenticate', 'Bearer realm="rakomi", error="invalid_token"');
95
+ res.status(401).json({
96
+ error: {
97
+ code: 'auth/guard_error',
98
+ message: 'Authorization check failed',
99
+ docs_url: 'https://docs.rakomi.dev/sdk/errors#guard-error',
100
+ },
101
+ });
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,30 @@
1
+ export type { AnonymousSigninOptions, AnonymousSigninResult } from './anonymous.js';
2
+ export { AnonymousSessionExpiredError, anonymousSignin, isAnonymousTokenHeuristic, maybeThrowAnonymousExpired, } from './anonymous.js';
3
+ export { RakomiClient } from './client.js';
4
+ export type { CreateDpopProverOptions, DpopProver } from './dpop.js';
5
+ export { createDpopProver } from './dpop.js';
6
+ export type { CreateDpopSessionOptions, DpopDowngradeInfo } from './dpop-session.js';
7
+ export { createDpopSession, DpopSession } from './dpop-session.js';
8
+ export type { AwaitDeviceTokensOptions, DeviceAuthorizationIssued, PollForDeviceTokenOptions, RunDeviceFlowOptions, StartDeviceAuthorizationOptions, } from './device.js';
9
+ export { awaitDeviceTokens, pollForDeviceToken, run as runDeviceFlow, startDeviceAuthorization, } from './device.js';
10
+ export { detectEnvironment } from './env-detect.js';
11
+ export type { ErrorCode } from './errors.js';
12
+ export { ACCOUNT_LINKING_IDENTITY_NOT_FOUND, ACCOUNT_LINKING_NETWORK_ERROR, ACCOUNT_LINKING_RATE_LIMITED, AccountLinkingDisabledError, ANONYMOUS_DISABLED, ANONYMOUS_MAU_EXHAUSTED, ANONYMOUS_NETWORK_ERROR, ANONYMOUS_RATE_LIMITED, AUTH_DPOP_PROVER_UNAVAILABLE, AUTH_DPOP_ROTATION_DID_NOT_TAKE, AUTH_DPOP_ROTATION_NOOP, AUTH_INVALID_DPOP_PROOF, AUTH_INVALID_REFRESH_TOKEN, AUTH_REFRESH_SUPERSEDED_BY_ROTATION, CannotUnlinkLastMethodError, CONFIG_INVALID_BASE_URL, CONFIG_MISSING_API_KEY, CONFIG_MISSING_WEBHOOK_SECRET, CooldownActiveError, ERROR_CODES, IdentityOwnedByOtherUserError, JWKS_FETCH_FAILED, JWKS_INVALID_RESPONSE, JWKS_NO_MATCHING_KEY, LinkStateExpiredError, MfaStepUpRequiredError, MfaStepUpUnavailableError, OAUTH_INVALID_CLIENT, OAUTH_INVALID_GRANT, OAUTH_INVALID_REQUEST, OAUTH_MISSING_CLIENT_ID, OAUTH_MISSING_CLIENT_SECRET, OAUTH_NETWORK_ERROR, OAUTH_UNSUPPORTED_GRANT_TYPE, RakomiError, TOKEN_EXPIRED, TOKEN_INVALID_ALGORITHM, TOKEN_INVALID_AUDIENCE, TOKEN_INVALID_ISSUER, TOKEN_INVALID_SIGNATURE, TOKEN_MALFORMED, TOKEN_MISSING_CLAIMS, TOKEN_NOT_YET_VALID, TOKEN_REVOKED, WEBHOOK_INVALID_BODY, WEBHOOK_INVALID_SECRET, WEBHOOK_INVALID_SIGNATURE, WEBHOOK_MISSING_HEADER, WEBHOOK_TIMESTAMP_EXPIRED, WEBHOOK_TIMESTAMP_TOO_NEW, WEBHOOK_TIMESTAMP_TOO_OLD, } from './errors.js';
13
+ export type { EidasLevel } from './eudi.js';
14
+ export { eidasLevel, isEudiVerified } from './eudi.js';
15
+ export type { CircuitState, FlagResult, FlagsAllResult, FlagsOptions, UserContext } from './flags.js';
16
+ export { FlagsClient } from './flags.js';
17
+ export { requirePermission, requireRole } from './guards.js';
18
+ export type { AccountLinkingProvider, LinkCallOptions, LinkedMethod, LinkedMethodsResponse, LinkedVia, LinkInitiateOptions, LinkInitiateResponse, UnlinkResponse, } from './link.js';
19
+ export { LinkClient } from './link.js';
20
+ export type { AgentsCallOptions, AgentsClientContext, ListUserAgentsResponse, RevokeUserAgentOptions, RevokeUserAgentResponse, UserAgentResponse, } from './agents.js';
21
+ export { AgentNotFoundError, AgentsClient, AgentsNetworkError, AgentsRateLimitedError, AgentsUnauthorizedError, } from './agents.js';
22
+ export { buildAuthorizeUrl, exchangeCode, generatePKCE, generateState, refreshToken, rotateRefreshKey, } from './oauth.js';
23
+ export { hasPermission, hasRole } from './rbac.js';
24
+ export type { TokenExchangeOptions, TokenExchangeResponse, } from './token-exchange.js';
25
+ export { exchangeTokenOrThrow, exchangeTokenViaApi, TokenExchangeError, TokenExchangeInvalidClientError, TokenExchangeInvalidGrantError, TokenExchangeInvalidScopeError, TokenExchangeRateLimitedError, TokenExchangeUnauthorizedClientError, } from './token-exchange.js';
26
+ export type { CibaAwaitDecisionOptions, CibaInitiateOptions, CibaInitiateResponse, CibaPollResponse, } from './ciba.js';
27
+ export { awaitCibaDecision, CibaAccessDeniedError, CibaAuthorizationPendingError, CibaError, CibaExpiredTokenError, CibaInvalidClientError, CibaInvalidRequestError, CibaInvalidScopeError, CibaReplayError, CibaSlowDownError, CibaUnauthorizedClientError, CibaUnknownUserError, CibaUserCapReachedError, initiateCiba, pollCiba, } from './ciba.js';
28
+ export type { AuthorizeUrlOptions, MiddlewareOptions, OAuthExchangeOptions, OAuthRefreshOptions, OAuthRotateOptions, OAuthTokenResponse, OrgMembership, PkceChallenge, PublisherEventType, PublisherWebhookEvent, PublisherWebhookEventType, PublisherWebhookVerifyData, RakomiConfig, RotationTokenResponse, SdkEnvironment, SdkError, TokenPayload, VerifyResult, WebhookEvent, WebhookHeaders, WebhookVerifyData, } from './types.js';
29
+ export { verifyPublisherWebhook } from './verify-publisher-webhook.js';
30
+ export { verifyWebhook } from './verify-webhook.js';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export { AnonymousSessionExpiredError, anonymousSignin, isAnonymousTokenHeuristic, maybeThrowAnonymousExpired, } from './anonymous.js';
2
+ export { RakomiClient } from './client.js';
3
+ export { createDpopProver } from './dpop.js';
4
+ export { createDpopSession, DpopSession } from './dpop-session.js';
5
+ export { awaitDeviceTokens, pollForDeviceToken, run as runDeviceFlow, startDeviceAuthorization, } from './device.js';
6
+ export { detectEnvironment } from './env-detect.js';
7
+ export { ACCOUNT_LINKING_IDENTITY_NOT_FOUND, ACCOUNT_LINKING_NETWORK_ERROR, ACCOUNT_LINKING_RATE_LIMITED, AccountLinkingDisabledError, ANONYMOUS_DISABLED, ANONYMOUS_MAU_EXHAUSTED, ANONYMOUS_NETWORK_ERROR, ANONYMOUS_RATE_LIMITED, AUTH_DPOP_PROVER_UNAVAILABLE, AUTH_DPOP_ROTATION_DID_NOT_TAKE, AUTH_DPOP_ROTATION_NOOP, AUTH_INVALID_DPOP_PROOF, AUTH_INVALID_REFRESH_TOKEN, AUTH_REFRESH_SUPERSEDED_BY_ROTATION, CannotUnlinkLastMethodError, CONFIG_INVALID_BASE_URL, CONFIG_MISSING_API_KEY, CONFIG_MISSING_WEBHOOK_SECRET, CooldownActiveError, ERROR_CODES, IdentityOwnedByOtherUserError, JWKS_FETCH_FAILED, JWKS_INVALID_RESPONSE, JWKS_NO_MATCHING_KEY, LinkStateExpiredError, MfaStepUpRequiredError, MfaStepUpUnavailableError, OAUTH_INVALID_CLIENT, OAUTH_INVALID_GRANT, OAUTH_INVALID_REQUEST, OAUTH_MISSING_CLIENT_ID, OAUTH_MISSING_CLIENT_SECRET, OAUTH_NETWORK_ERROR, OAUTH_UNSUPPORTED_GRANT_TYPE, RakomiError, TOKEN_EXPIRED, TOKEN_INVALID_ALGORITHM, TOKEN_INVALID_AUDIENCE, TOKEN_INVALID_ISSUER, TOKEN_INVALID_SIGNATURE, TOKEN_MALFORMED, TOKEN_MISSING_CLAIMS, TOKEN_NOT_YET_VALID, TOKEN_REVOKED, WEBHOOK_INVALID_BODY, WEBHOOK_INVALID_SECRET, WEBHOOK_INVALID_SIGNATURE, WEBHOOK_MISSING_HEADER, WEBHOOK_TIMESTAMP_EXPIRED, WEBHOOK_TIMESTAMP_TOO_NEW, WEBHOOK_TIMESTAMP_TOO_OLD, } from './errors.js';
8
+ export { eidasLevel, isEudiVerified } from './eudi.js';
9
+ export { FlagsClient } from './flags.js';
10
+ export { requirePermission, requireRole } from './guards.js';
11
+ export { LinkClient } from './link.js';
12
+ export { AgentNotFoundError, AgentsClient, AgentsNetworkError, AgentsRateLimitedError, AgentsUnauthorizedError, } from './agents.js';
13
+ export { buildAuthorizeUrl, exchangeCode, generatePKCE, generateState, refreshToken, rotateRefreshKey, } from './oauth.js';
14
+ export { hasPermission, hasRole } from './rbac.js';
15
+ export { exchangeTokenOrThrow, exchangeTokenViaApi, TokenExchangeError, TokenExchangeInvalidClientError, TokenExchangeInvalidGrantError, TokenExchangeInvalidScopeError, TokenExchangeRateLimitedError, TokenExchangeUnauthorizedClientError, } from './token-exchange.js';
16
+ export { awaitCibaDecision, CibaAccessDeniedError, CibaAuthorizationPendingError, CibaError, CibaExpiredTokenError, CibaInvalidClientError, CibaInvalidRequestError, CibaInvalidScopeError, CibaReplayError, CibaSlowDownError, CibaUnauthorizedClientError, CibaUnknownUserError, CibaUserCapReachedError, initiateCiba, pollCiba, } from './ciba.js';
17
+ export { verifyPublisherWebhook } from './verify-publisher-webhook.js';
18
+ export { verifyWebhook } from './verify-webhook.js';
@@ -0,0 +1,13 @@
1
+ export type CanonicalUrlVersion = 'rfc9449-2023';
2
+ export interface CanonicalizeUrlOptions {
3
+ version?: CanonicalUrlVersion;
4
+ /** Trusted-proxy hint: scheme override from `X-Forwarded-Proto`. */
5
+ forwardedProto?: string | null | undefined;
6
+ /** Trusted-proxy hint: host override from `X-Forwarded-Host`. */
7
+ forwardedHost?: string | null | undefined;
8
+ }
9
+ export declare class CanonicalizeUrlError extends Error {
10
+ readonly code: 'invalid_url' | 'unsupported_version';
11
+ constructor(code: 'invalid_url' | 'unsupported_version', message: string);
12
+ }
13
+ export declare function canonicalizeUrl(input: string, options?: CanonicalizeUrlOptions): string;
@@ -0,0 +1,52 @@
1
+ export class CanonicalizeUrlError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'CanonicalizeUrlError';
7
+ }
8
+ }
9
+ function collapsePath(path) {
10
+ const collapsed = path.replace(/\/{2,}/g, '/');
11
+ if (collapsed.length > 1 && collapsed.endsWith('/')) {
12
+ return collapsed.slice(0, -1);
13
+ }
14
+ return collapsed;
15
+ }
16
+ function lowercasePercentHex(s) {
17
+ return s.replace(/%([0-9A-Fa-f]{2})/g, (_m, h) => `%${h.toLowerCase()}`);
18
+ }
19
+ export function canonicalizeUrl(input, options = {}) {
20
+ const version = options.version ?? 'rfc9449-2023';
21
+ if (version !== 'rfc9449-2023') {
22
+ throw new CanonicalizeUrlError('unsupported_version', `unsupported canonicalize-url version: ${version}`);
23
+ }
24
+ let url;
25
+ try {
26
+ url = new URL(input);
27
+ }
28
+ catch {
29
+ throw new CanonicalizeUrlError('invalid_url', 'invalid url');
30
+ }
31
+ if (options.forwardedProto) {
32
+ const proto = options.forwardedProto.toLowerCase().trim();
33
+ if (proto === 'http' || proto === 'https') {
34
+ url.protocol = `${proto}:`;
35
+ }
36
+ }
37
+ if (options.forwardedHost) {
38
+ const first = options.forwardedHost.split(',')[0].trim();
39
+ if (first.length > 0) {
40
+ url.host = first;
41
+ }
42
+ }
43
+ if ((url.protocol === 'https:' && url.port === '443') ||
44
+ (url.protocol === 'http:' && url.port === '80')) {
45
+ url.port = '';
46
+ }
47
+ url.search = '';
48
+ url.hash = '';
49
+ const normalizedPath = collapsePath(lowercasePercentHex(url.pathname));
50
+ const host = url.host;
51
+ return `${url.protocol}//${host}${normalizedPath}`;
52
+ }
@@ -0,0 +1,3 @@
1
+ export declare const TOKEN_EXCHANGE_GRANT_TYPE: "urn:ietf:params:oauth:grant-type:token-exchange";
2
+ export declare const TOKEN_EXCHANGE_ACCESS_TOKEN_TYPE: "urn:ietf:params:oauth:token-type:access_token";
3
+ export declare const CIBA_GRANT_TYPE: "urn:openid:params:grant-type:ciba";
@@ -0,0 +1,3 @@
1
+ export const TOKEN_EXCHANGE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange';
2
+ export const TOKEN_EXCHANGE_ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
3
+ export const CIBA_GRANT_TYPE = 'urn:openid:params:grant-type:ciba';
@@ -0,0 +1,31 @@
1
+ import type { CryptoKey as JoseCryptoKey } from 'jose';
2
+ import type { SdkError } from './types.js';
3
+ type CacheResult<T> = {
4
+ ok: true;
5
+ data: T;
6
+ } | {
7
+ ok: false;
8
+ error: SdkError;
9
+ };
10
+ export declare class JwksCache {
11
+ private cache;
12
+ private refreshPromise;
13
+ private readonly jwksUrl;
14
+ private readonly baseUrl;
15
+ constructor(baseUrl: string);
16
+ /**
17
+ * Get the base URL of this Rakomi deployment.
18
+ * Used to derive the expected JWT audience claim value.
19
+ */
20
+ getBaseUrl(): string;
21
+ /**
22
+ * Get the revocation epoch from the last JWKS response.
23
+ * Returns null if no revocation has occurred or JWKS hasn't been fetched yet.
24
+ */
25
+ getRevocationEpoch(): number | null;
26
+ getKey(kid: string): Promise<CacheResult<JoseCryptoKey>>;
27
+ refresh(): Promise<CacheResult<void>>;
28
+ private isExpired;
29
+ private doRefresh;
30
+ }
31
+ export {};
@@ -0,0 +1,135 @@
1
+ import { importJWK } from 'jose';
2
+ import { JWKS_FETCH_FAILED, JWKS_INVALID_RESPONSE, JWKS_NO_MATCHING_KEY } from './errors.js';
3
+ const DEFAULT_MAX_AGE = 3600;
4
+ export class JwksCache {
5
+ cache = null;
6
+ refreshPromise = null;
7
+ jwksUrl;
8
+ baseUrl;
9
+ constructor(baseUrl) {
10
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
11
+ this.jwksUrl = `${this.baseUrl}/.well-known/jwks.json`;
12
+ }
13
+ /**
14
+ * Get the base URL of this Rakomi deployment.
15
+ * Used to derive the expected JWT audience claim value.
16
+ */
17
+ getBaseUrl() {
18
+ return this.baseUrl;
19
+ }
20
+ /**
21
+ * Get the revocation epoch from the last JWKS response.
22
+ * Returns null if no revocation has occurred or JWKS hasn't been fetched yet.
23
+ */
24
+ getRevocationEpoch() {
25
+ return this.cache?.revocationEpoch ?? null;
26
+ }
27
+ async getKey(kid) {
28
+ if (this.cache && !this.isExpired()) {
29
+ const entry = this.cache.keys.find((k) => k.kid === kid);
30
+ if (entry) {
31
+ return { ok: true, data: entry.key };
32
+ }
33
+ }
34
+ const refreshResult = await this.refresh();
35
+ if (!refreshResult.ok) {
36
+ return refreshResult;
37
+ }
38
+ const entry = this.cache?.keys.find((k) => k.kid === kid);
39
+ if (!entry) {
40
+ return { ok: false, error: JWKS_NO_MATCHING_KEY() };
41
+ }
42
+ return { ok: true, data: entry.key };
43
+ }
44
+ async refresh() {
45
+ if (this.refreshPromise) {
46
+ return this.refreshPromise;
47
+ }
48
+ this.refreshPromise = this.doRefresh();
49
+ try {
50
+ return await this.refreshPromise;
51
+ }
52
+ finally {
53
+ this.refreshPromise = null;
54
+ }
55
+ }
56
+ isExpired() {
57
+ if (!this.cache)
58
+ return true;
59
+ const elapsed = (Date.now() - this.cache.fetchedAt) / 1000;
60
+ return elapsed >= this.cache.maxAge;
61
+ }
62
+ async doRefresh() {
63
+ let response;
64
+ try {
65
+ response = await fetch(this.jwksUrl, {
66
+ redirect: 'error',
67
+ signal: AbortSignal.timeout(5000),
68
+ });
69
+ }
70
+ catch (err) {
71
+ if (this.cache) {
72
+ return { ok: true, data: undefined };
73
+ }
74
+ const detail = err instanceof Error
75
+ ? (err.name === 'TimeoutError' ? 'Request timeout' : err.message)
76
+ : 'Network error';
77
+ return { ok: false, error: JWKS_FETCH_FAILED(detail) };
78
+ }
79
+ if (!response.ok) {
80
+ if (this.cache) {
81
+ return { ok: true, data: undefined };
82
+ }
83
+ return { ok: false, error: JWKS_FETCH_FAILED(`HTTP ${response.status}`) };
84
+ }
85
+ let body;
86
+ try {
87
+ body = await response.json();
88
+ }
89
+ catch {
90
+ return { ok: false, error: JWKS_INVALID_RESPONSE() };
91
+ }
92
+ if (!body ||
93
+ typeof body !== 'object' ||
94
+ !('keys' in body) ||
95
+ !Array.isArray(body.keys)) {
96
+ return { ok: false, error: JWKS_INVALID_RESPONSE() };
97
+ }
98
+ const bodyObj = body;
99
+ const jwks = bodyObj.keys;
100
+ let revocationEpoch = null;
101
+ if (typeof bodyObj.revocation_epoch === 'number' && Number.isInteger(bodyObj.revocation_epoch) && bodyObj.revocation_epoch > 0) {
102
+ revocationEpoch = bodyObj.revocation_epoch;
103
+ }
104
+ const entries = [];
105
+ for (const jwk of jwks) {
106
+ if (jwk.alg === 'RS256' && jwk.use === 'sig' && typeof jwk.kid === 'string') {
107
+ try {
108
+ const key = await importJWK(jwk, 'RS256');
109
+ if (!(key instanceof Uint8Array)) {
110
+ entries.push({ kid: jwk.kid, key });
111
+ }
112
+ }
113
+ catch {
114
+ }
115
+ }
116
+ }
117
+ const maxAge = parseCacheControlMaxAge(response.headers.get('Cache-Control'));
118
+ this.cache = {
119
+ keys: entries,
120
+ revocationEpoch,
121
+ fetchedAt: Date.now(),
122
+ maxAge,
123
+ };
124
+ return { ok: true, data: undefined };
125
+ }
126
+ }
127
+ function parseCacheControlMaxAge(header) {
128
+ if (!header)
129
+ return DEFAULT_MAX_AGE;
130
+ const match = header.match(/max-age=(\d+)/);
131
+ if (!match?.[1])
132
+ return DEFAULT_MAX_AGE;
133
+ const value = parseInt(match[1], 10);
134
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_MAX_AGE;
135
+ }
package/dist/link.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Node SDK surface for user-scoped account linking.
3
+ *
4
+ * Wraps the three `/v1/users/me/link*` endpoints.
5
+ * These endpoints require an end-user JWT (NOT an API key), so each helper
6
+ * takes a `{ userToken }` option. The SDK client (constructed with an API key)
7
+ * only carries the `baseUrl`; the user token flows per-call.
8
+ *
9
+ * Returns the shared `VerifyResult` shape — NEVER throws on expected 4xx.
10
+ * Typed error classes (AccountLinkingDisabledError, IdentityOwnedByOtherUserError,
11
+ * CannotUnlinkLastMethodError) are also exported for callers that prefer
12
+ * pattern-matching via `instanceof` over inspecting `error.code`.
13
+ */
14
+ import type { VerifyResult } from './types.js';
15
+ export type AccountLinkingProvider = 'google' | 'github' | 'microsoft' | 'apple' | 'discord' | 'facebook' | 'slack' | 'twitter' | 'gitlab' | 'linkedin';
16
+ export type LinkedVia = 'signup' | 'explicit_link' | 'automatic_link';
17
+ export type LinkedMethod = {
18
+ kind: 'password';
19
+ active: boolean;
20
+ } | {
21
+ kind: 'social';
22
+ provider: AccountLinkingProvider;
23
+ provider_email_hash: string;
24
+ linked_at: string;
25
+ linked_via: LinkedVia;
26
+ } | {
27
+ kind: 'passkey';
28
+ count: number;
29
+ };
30
+ export interface LinkedMethodsResponse {
31
+ methods: LinkedMethod[];
32
+ cooldown_until: string | null;
33
+ }
34
+ export interface LinkInitiateResponse {
35
+ authorization_url: string;
36
+ }
37
+ export interface UnlinkResponse {
38
+ unlinked: boolean;
39
+ provider: AccountLinkingProvider;
40
+ warnings: Array<'only_password_remains'>;
41
+ }
42
+ export interface LinkCallOptions {
43
+ /** End-user JWT (Bearer). REQUIRED — these endpoints do NOT accept API keys. */
44
+ userToken: string;
45
+ }
46
+ export interface LinkInitiateOptions extends LinkCallOptions {
47
+ redirectUri: string;
48
+ /** Optional MFA step-up token (forward path — server-side gate lives in). */
49
+ mfaVerificationToken?: string;
50
+ }
51
+ export interface LinkClientContext {
52
+ baseUrl: string;
53
+ fetchImpl?: typeof fetch;
54
+ }
55
+ /**
56
+ * User-scoped account-linking resource. Attached to `RakomiClient#link`.
57
+ *
58
+ * All methods require an end-user JWT passed via `{ userToken }`. The underlying
59
+ * `RakomiClient` API key is NOT sent on these calls — the API rejects API-key
60
+ * auth on user-scoped routes.
61
+ */
62
+ export declare class LinkClient {
63
+ private readonly baseUrl;
64
+ private readonly fetchImpl;
65
+ constructor(ctx: LinkClientContext);
66
+ /** GET /v1/users/me/link — list linked methods for the authenticated user. */
67
+ list(options: LinkCallOptions): Promise<VerifyResult<LinkedMethodsResponse>>;
68
+ /** POST /v1/users/me/link/{provider} — initiate OAuth link flow. */
69
+ initiate(provider: AccountLinkingProvider, options: LinkInitiateOptions): Promise<VerifyResult<LinkInitiateResponse>>;
70
+ /** DELETE /v1/users/me/link/{provider} — unlink a social identity. */
71
+ remove(provider: AccountLinkingProvider, options: LinkCallOptions): Promise<VerifyResult<UnlinkResponse>>;
72
+ private mapError;
73
+ }