@almadar/integrations 2.38.0 → 2.40.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.
@@ -1,5 +1,5 @@
1
1
  import { g as IntegrationParams, I as IntegrationCallContext, d as IntegrationFactory } from '../BaseIntegration-CWdas-N_.js';
2
- import { o as IntegrationName, m as IntegrationActionName, n as IntegrationContracts, e as CredentialStore } from '../store-RNvxZO-q.js';
2
+ import { o as IntegrationName, m as IntegrationActionName, n as IntegrationContracts, e as CredentialStore } from '../store-DG5OGiuQ.js';
3
3
  import '@almadar/core';
4
4
 
5
5
  /**
@@ -50,7 +50,15 @@ declare class RuntimeIntegrationManager {
50
50
  private credentialStore;
51
51
  private storeEnvBase;
52
52
  private storeFirst;
53
- constructor();
53
+ private readonly tenant;
54
+ /**
55
+ * `tenant`: one tenant of a multi-tenant host. Its services are configured from its credential
56
+ * store alone (never the host's env), and it installs nothing process-wide; resolve its
57
+ * references inside `runWithCredentialScope`.
58
+ */
59
+ constructor(options?: {
60
+ tenant?: boolean;
61
+ });
54
62
  /**
55
63
  * W4: configure with the tenant credential store layered over the env —
56
64
  * store → env → unconfigured. Installs the store as the process-wide
@@ -1,6 +1,7 @@
1
1
  import { createLogger } from '@almadar/logger';
2
2
  import { integratorsRegistry } from '@almadar/core/patterns';
3
3
  import { createHmac, createHash } from 'crypto';
4
+ import { AsyncLocalStorage } from 'async_hooks';
4
5
  import { mkdtempSync, writeFileSync, rmSync, promises } from 'fs';
5
6
  import { join } from 'path';
6
7
  import Stripe2 from 'stripe';
@@ -133,8 +134,8 @@ var serviceCredentials = {
133
134
  { envVar: "REDIS_URL", required: true, description: "Redis connection URL" }
134
135
  ],
135
136
  oauth: [
136
- { envVar: "OAUTH_CLIENT_ID", required: true, description: "OIDC client ID" },
137
- { envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OIDC client secret" },
137
+ { envVar: "OAUTH_CLIENT_ID", required: true, description: "OAuth/OIDC client ID" },
138
+ { envVar: "OAUTH_CLIENT_SECRET", required: true, description: "OAuth/OIDC client secret" },
138
139
  { envVar: "OAUTH_REDIRECT_URI", required: false, description: "OIDC redirect URI (default per-call param)" },
139
140
  { envVar: "OIDC_ISSUER_URL", required: false, description: "OIDC issuer URL (default https://accounts.google.com)" }
140
141
  ],
@@ -400,8 +401,7 @@ function getIntegration(name) {
400
401
  function getRegisteredIntegrations() {
401
402
  return Object.keys(INTEGRATION_REGISTRY);
402
403
  }
403
-
404
- // src/factory.ts
404
+ var callLog = createLogger("almadar:integrations:calls");
405
405
  function instanceKey(name, principal) {
406
406
  return principal ? `${name}\0${principal}` : name;
407
407
  }
@@ -446,8 +446,16 @@ var IntegrationFactory = class {
446
446
  * Execute an action on an integration
447
447
  */
448
448
  async execute(integration, action, params, context) {
449
- const instance = this.get(integration, context?.principal);
450
- return await instance.execute(action, params, context);
449
+ const start = performance.now();
450
+ const record = (ok, error) => callLog.call({ kind: "external", service: integration, op: action, durationMs: performance.now() - start, ok, ...error !== void 0 ? { error } : {} });
451
+ try {
452
+ const result = await this.get(integration, context?.principal).execute(action, params, context);
453
+ record(result.success, result.success ? void 0 : result.error?.message);
454
+ return result;
455
+ } catch (err) {
456
+ record(false, err instanceof Error ? err.message : String(err));
457
+ throw err;
458
+ }
451
459
  }
452
460
  /**
453
461
  * Check if integration is configured
@@ -491,9 +499,8 @@ var IntegrationFactory = class {
491
499
  this.configs.clear();
492
500
  }
493
501
  };
494
-
495
- // src/credentials/resolver.ts
496
502
  var installedStore = null;
503
+ var tenantScope = new AsyncLocalStorage();
497
504
  var activeFactory = null;
498
505
  function installActiveFactory(factory) {
499
506
  activeFactory = factory;
@@ -508,6 +515,8 @@ function getInstalledCredentialStore() {
508
515
  return installedStore;
509
516
  }
510
517
  function resolveCredentialRef(ref, env = process.env) {
518
+ const tenant = tenantScope.getStore();
519
+ if (tenant) return tenant.resolve(ref);
511
520
  return installedStore?.resolve(ref) ?? env[ref];
512
521
  }
513
522
  join(".almadar", "dev-credentials.json");
@@ -3547,6 +3556,31 @@ var PROVIDER_AUTH_URLS = {
3547
3556
  var PROVIDER_ISSUERS = {
3548
3557
  google: "https://accounts.google.com"
3549
3558
  };
3559
+ var PROVIDER_SERVER_METADATA = {
3560
+ github: {
3561
+ issuer: "https://github.com",
3562
+ authorization_endpoint: "https://github.com/login/oauth/authorize",
3563
+ token_endpoint: "https://github.com/login/oauth/access_token"
3564
+ }
3565
+ };
3566
+ var OAuthGrantNotFoundError = class extends Error {
3567
+ constructor(state) {
3568
+ super(`Invalid or expired state token: ${state}`);
3569
+ this.name = "OAuthGrantNotFoundError";
3570
+ }
3571
+ };
3572
+ var OAuthSubjectMismatchError = class extends Error {
3573
+ constructor() {
3574
+ super("OAuth completion refused: the completing subject is not the one that started the authorization");
3575
+ this.name = "OAuthSubjectMismatchError";
3576
+ }
3577
+ };
3578
+ function assertSubject(bound, completer) {
3579
+ if (bound !== completer) throw new OAuthSubjectMismatchError();
3580
+ }
3581
+ function grantedScopes(scope) {
3582
+ return (scope ?? "").split(/[\s,]+/).filter((s) => s.length > 0);
3583
+ }
3550
3584
  var PENDING_GRANT_TTL_MS = 10 * 60 * 1e3;
3551
3585
  var InMemoryPendingGrantStore = class {
3552
3586
  constructor() {
@@ -3571,7 +3605,7 @@ var InMemoryPendingGrantStore = class {
3571
3605
  var OAuthIntegration = class extends BaseIntegration {
3572
3606
  constructor(config) {
3573
3607
  super(config);
3574
- /** Maps state token -> provider for pending MOCK authorization flows */
3608
+ /** Maps state token -> pending MOCK authorization */
3575
3609
  this.states = /* @__PURE__ */ new Map();
3576
3610
  /** Maps access token -> token set (mock backend) */
3577
3611
  this.tokens = /* @__PURE__ */ new Map();
@@ -3600,12 +3634,25 @@ var OAuthIntegration = class extends BaseIntegration {
3600
3634
  try {
3601
3635
  let data;
3602
3636
  switch (action) {
3603
- case "authorize":
3604
- data = await this.executeWithRetry(() => this.real ? this.oidcAuthorize(params) : this.authorize(params));
3637
+ case "authorize": {
3638
+ const input = {
3639
+ provider: params.provider,
3640
+ scopes: params.scopes,
3641
+ redirectUri: params.redirectUri,
3642
+ ...params.subject === void 0 ? {} : { subject: params.subject }
3643
+ };
3644
+ data = await this.executeWithRetry(() => this.startAuthorization(input));
3605
3645
  break;
3606
- case "token":
3607
- data = await this.executeWithRetry(() => this.real ? this.oidcToken(params) : this.token(params));
3646
+ }
3647
+ case "token": {
3648
+ const input = {
3649
+ code: params.code,
3650
+ state: params.state,
3651
+ ...params.subject === void 0 ? {} : { subject: params.subject }
3652
+ };
3653
+ data = await this.executeWithRetry(() => this.completeAuthorization(input));
3608
3654
  break;
3655
+ }
3609
3656
  case "refresh":
3610
3657
  data = await this.executeWithRetry(() => this.real ? this.oidcRefresh(params) : this.refresh(params));
3611
3658
  break;
@@ -3627,6 +3674,14 @@ var OAuthIntegration = class extends BaseIntegration {
3627
3674
  return this.handleError(action, error);
3628
3675
  }
3629
3676
  }
3677
+ /** Build the provider authorization URL and hold the pending grant (PKCE verifier, subject) under `state`. */
3678
+ async startAuthorization(input) {
3679
+ return this.real ? this.oidcAuthorize(input) : this.authorize(input);
3680
+ }
3681
+ /** Consume the pending grant once and exchange the code; throws on an unknown, replayed or expired `state`. */
3682
+ async completeAuthorization(input) {
3683
+ return this.real ? this.oidcToken(input) : this.token(input);
3684
+ }
3630
3685
  // ---------------------------------------------------------------------------
3631
3686
  // OIDC backend (openid-client)
3632
3687
  // ---------------------------------------------------------------------------
@@ -3642,6 +3697,19 @@ var OAuthIntegration = class extends BaseIntegration {
3642
3697
  return issuer;
3643
3698
  }
3644
3699
  async configurationFor(provider) {
3700
+ const metadata = PROVIDER_SERVER_METADATA[provider];
3701
+ if (metadata && !this.config.env.OIDC_ISSUER_URL) {
3702
+ const cached2 = this.discovered.get(metadata.issuer);
3703
+ if (cached2) return cached2;
3704
+ const configuration2 = new oidc.Configuration(
3705
+ metadata,
3706
+ this.config.env.OAUTH_CLIENT_ID,
3707
+ this.config.env.OAUTH_CLIENT_SECRET,
3708
+ oidc.ClientSecretPost(this.config.env.OAUTH_CLIENT_SECRET)
3709
+ );
3710
+ this.discovered.set(metadata.issuer, configuration2);
3711
+ return configuration2;
3712
+ }
3645
3713
  const issuer = this.issuerFor(provider);
3646
3714
  const cached = this.discovered.get(issuer);
3647
3715
  if (cached) return cached;
@@ -3653,10 +3721,9 @@ var OAuthIntegration = class extends BaseIntegration {
3653
3721
  this.discovered.set(issuer, configuration);
3654
3722
  return configuration;
3655
3723
  }
3656
- async oidcAuthorize(params) {
3657
- const provider = params.provider;
3658
- const scopes = params.scopes;
3659
- const redirectUri = params.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
3724
+ async oidcAuthorize(input) {
3725
+ const { provider, scopes, subject } = input;
3726
+ const redirectUri = input.redirectUri || this.config.env.OAUTH_REDIRECT_URI;
3660
3727
  const configuration = await this.configurationFor(provider);
3661
3728
  const state = oidc.randomState();
3662
3729
  const pkceVerifier = oidc.randomPKCECodeVerifier();
@@ -3673,17 +3740,21 @@ var OAuthIntegration = class extends BaseIntegration {
3673
3740
  parameters.prompt = "consent";
3674
3741
  }
3675
3742
  const authUrl = oidc.buildAuthorizationUrl(configuration, parameters);
3676
- await this.pendingStore().put(state, { provider, redirectUri, pkceVerifier }, PENDING_GRANT_TTL_MS);
3743
+ await this.pendingStore().put(
3744
+ state,
3745
+ { provider, redirectUri, pkceVerifier, ...subject === void 0 ? {} : { subject } },
3746
+ PENDING_GRANT_TTL_MS
3747
+ );
3677
3748
  void this.pendingStore().sweep();
3678
3749
  return { authUrl: authUrl.toString(), state };
3679
3750
  }
3680
- async oidcToken(params) {
3681
- const code = params.code;
3682
- const state = params.state;
3751
+ async oidcToken(input) {
3752
+ const { code, state } = input;
3683
3753
  const pendingAuth = await this.pendingStore().take(state);
3684
3754
  if (!pendingAuth) {
3685
- throw new Error(`Invalid or expired state token: ${state}`);
3755
+ throw new OAuthGrantNotFoundError(state);
3686
3756
  }
3757
+ assertSubject(pendingAuth.subject, input.subject);
3687
3758
  const configuration = await this.configurationFor(pendingAuth.provider);
3688
3759
  const callbackUrl = new URL(pendingAuth.redirectUri);
3689
3760
  callbackUrl.searchParams.set("code", code);
@@ -3700,7 +3771,9 @@ var OAuthIntegration = class extends BaseIntegration {
3700
3771
  accessToken: tokens.access_token,
3701
3772
  refreshToken: tokens.refresh_token ?? "",
3702
3773
  expiresIn: tokens.expires_in ?? 3600,
3703
- tokenType: "bearer"
3774
+ tokenType: "bearer",
3775
+ scope: grantedScopes(tokens.scope),
3776
+ ...pendingAuth.subject === void 0 ? {} : { subject: pendingAuth.subject }
3704
3777
  };
3705
3778
  }
3706
3779
  async oidcRefresh(params) {
@@ -3763,13 +3836,11 @@ var OAuthIntegration = class extends BaseIntegration {
3763
3836
  // ---------------------------------------------------------------------------
3764
3837
  // Mock backend actions
3765
3838
  // ---------------------------------------------------------------------------
3766
- async authorize(params) {
3767
- const provider = params.provider;
3768
- const scopes = params.scopes;
3769
- const redirectUri = params.redirectUri;
3839
+ async authorize(input) {
3840
+ const { provider, scopes, redirectUri, subject } = input;
3770
3841
  this.logger.debug("OAuth AUTHORIZE", { provider, scopes: scopes.join(", "), redirectUri });
3771
3842
  const state = this.generateToken(16);
3772
- this.states.set(state, provider);
3843
+ this.states.set(state, { provider, scopes, ...subject === void 0 ? {} : { subject } });
3773
3844
  const baseUrl = PROVIDER_AUTH_URLS[provider];
3774
3845
  const queryParams = new URLSearchParams({
3775
3846
  response_type: "code",
@@ -3781,17 +3852,15 @@ var OAuthIntegration = class extends BaseIntegration {
3781
3852
  const authUrl = `${baseUrl}?${queryParams.toString()}`;
3782
3853
  return { authUrl, state };
3783
3854
  }
3784
- async token(params) {
3785
- const code = params.code;
3786
- const state = params.state;
3855
+ async token(input) {
3856
+ const { code, state } = input;
3787
3857
  this.logger.debug("OAuth TOKEN", { code, state });
3788
- const provider = this.states.get(state);
3789
- if (!provider) {
3790
- throw new Error(
3791
- `Invalid or expired state token: ${state}`
3792
- );
3858
+ const pending = this.states.get(state);
3859
+ if (!pending) {
3860
+ throw new OAuthGrantNotFoundError(state);
3793
3861
  }
3794
3862
  this.states.delete(state);
3863
+ assertSubject(pending.subject, input.subject);
3795
3864
  const accessToken = this.generateToken(32);
3796
3865
  const refreshToken = this.generateToken(32);
3797
3866
  const expiresIn = 3600;
@@ -3803,8 +3872,15 @@ var OAuthIntegration = class extends BaseIntegration {
3803
3872
  this.tokens.set(accessToken, tokenSet);
3804
3873
  this.refreshIndex.set(refreshToken, accessToken);
3805
3874
  const sub = this.generateToken(8);
3806
- this.sessions.set(accessToken, this.generateMockUser(provider, sub));
3807
- return { accessToken, refreshToken, expiresIn, tokenType: "bearer" };
3875
+ this.sessions.set(accessToken, this.generateMockUser(pending.provider, sub));
3876
+ return {
3877
+ accessToken,
3878
+ refreshToken,
3879
+ expiresIn,
3880
+ tokenType: "bearer",
3881
+ scope: pending.scopes,
3882
+ ...pending.subject === void 0 ? {} : { subject: pending.subject }
3883
+ };
3808
3884
  }
3809
3885
  async refresh(params) {
3810
3886
  const refreshToken = params.refreshToken;
@@ -5370,12 +5446,18 @@ function generateMockFromShape(shape) {
5370
5446
  return data;
5371
5447
  }
5372
5448
  var RuntimeIntegrationManager = class {
5373
- constructor() {
5449
+ /**
5450
+ * `tenant`: one tenant of a multi-tenant host. Its services are configured from its credential
5451
+ * store alone (never the host's env), and it installs nothing process-wide; resolve its
5452
+ * references inside `runWithCredentialScope`.
5453
+ */
5454
+ constructor(options = {}) {
5374
5455
  this.credentialStore = null;
5375
5456
  this.storeEnvBase = null;
5376
5457
  this.storeFirst = false;
5377
5458
  this.factory = new IntegrationFactory();
5378
- installActiveFactory(this.factory);
5459
+ this.tenant = options.tenant === true;
5460
+ if (!this.tenant) installActiveFactory(this.factory);
5379
5461
  }
5380
5462
  /**
5381
5463
  * W4: configure with the tenant credential store layered over the env —
@@ -5387,7 +5469,13 @@ var RuntimeIntegrationManager = class {
5387
5469
  */
5388
5470
  async configureFromStore(store, envOverride) {
5389
5471
  this.credentialStore = store;
5390
- this.storeEnvBase = envOverride ?? process.env;
5472
+ this.storeEnvBase = this.tenant ? {} : envOverride ?? process.env;
5473
+ if (this.tenant) {
5474
+ await store.warm();
5475
+ store.subscribe(() => this.refreshFromStore());
5476
+ this.refreshFromStore();
5477
+ return;
5478
+ }
5391
5479
  this.storeFirst = this.storeEnvBase.ALMADAR_CREDENTIALS_SOURCE === "store";
5392
5480
  installCredentialStore(store);
5393
5481
  await store.warm();