@usearete/sdk 0.3.0 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -512,6 +512,7 @@ const MIN_REFRESH_DELAY_MS = 1000;
512
512
  const DEFAULT_QUERY_PARAMETER = 'hs_token';
513
513
  const DEFAULT_HOSTED_TOKEN_ENDPOINT = 'https://api.arete.run/ws/sessions';
514
514
  const HOSTED_WEBSOCKET_SUFFIX = '.stack.arete.run';
515
+ const MAX_HTTP_AUTH_TOKEN_STATES = 32;
515
516
  function normalizeTokenResult(result) {
516
517
  if (typeof result === 'string') {
517
518
  return { token: result };
@@ -550,6 +551,71 @@ function parseJwtExpiry(token) {
550
551
  function normalizeExpiryTimestamp(expiresAt, expires_at) {
551
552
  return expiresAt ?? expires_at;
552
553
  }
554
+ function normalizeScopes(scopes) {
555
+ return [...new Set(scopes)].sort();
556
+ }
557
+ function isProgramReadBindingAuthTarget(value) {
558
+ if (typeof value !== 'object' || value === null) {
559
+ return false;
560
+ }
561
+ const candidate = value;
562
+ return candidate['targetKind'] === 'program-read-binding'
563
+ && typeof candidate['targetId'] === 'string'
564
+ && typeof candidate['programReleaseHash'] === 'string';
565
+ }
566
+ function isAuthTokenTarget(value) {
567
+ if (isProgramReadBindingAuthTarget(value)) {
568
+ return true;
569
+ }
570
+ if (typeof value !== 'object' || value === null) {
571
+ return false;
572
+ }
573
+ const candidate = value;
574
+ return candidate['targetKind'] === 'solana-gateway-binding'
575
+ && typeof candidate['targetId'] === 'string'
576
+ && candidate['programReleaseHash'] === undefined;
577
+ }
578
+ function isAuthTokenRequest(value) {
579
+ return typeof value === 'object'
580
+ && value !== null
581
+ && Array.isArray(value.scopes);
582
+ }
583
+ function hasAuthTokenTarget(request) {
584
+ return isAuthTokenTarget(request);
585
+ }
586
+ function normalizeAuthTokenRequest(request) {
587
+ const scopes = normalizeScopes(request.scopes);
588
+ const hasTargetField = request.targetKind !== undefined
589
+ || request.targetId !== undefined
590
+ || request.programReleaseHash !== undefined;
591
+ if (!hasTargetField) {
592
+ return { scopes };
593
+ }
594
+ if (!hasAuthTokenTarget(request)) {
595
+ throw new AreteError('Targeted authentication requires a complete supported target identity', 'INVALID_CONFIG');
596
+ }
597
+ if (isProgramReadBindingAuthTarget(request)) {
598
+ return {
599
+ scopes,
600
+ targetKind: request.targetKind,
601
+ targetId: request.targetId,
602
+ programReleaseHash: request.programReleaseHash,
603
+ };
604
+ }
605
+ return {
606
+ scopes,
607
+ targetKind: request.targetKind,
608
+ targetId: request.targetId,
609
+ };
610
+ }
611
+ function createTargetedHttpTokenIdentity(request) {
612
+ return JSON.stringify([
613
+ request.targetKind,
614
+ request.targetId,
615
+ request.programReleaseHash ?? null,
616
+ normalizeScopes(request.scopes),
617
+ ]);
618
+ }
553
619
  function isRefreshAuthResponseMessage(value) {
554
620
  if (typeof value !== 'object' || value === null) {
555
621
  return false;
@@ -594,6 +660,7 @@ class ConnectionManager {
594
660
  this.tokenRefreshTimeout = null;
595
661
  this.tokenRefreshInFlight = null;
596
662
  this.tokenRequestInFlight = null;
663
+ this.httpAuthTokenStates = new Map();
597
664
  this.currentState = 'disconnected';
598
665
  this.subscriptionQueue = new Map();
599
666
  this.activeSubscriptions = new Map();
@@ -615,6 +682,12 @@ class ConnectionManager {
615
682
  this.maxReconnectAttempts =
616
683
  config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
617
684
  this.authConfig = config.auth;
685
+ this.authFetch = config.fetch ?? ((input, init) => {
686
+ if (typeof globalThis.fetch !== 'function') {
687
+ throw new AreteError('A fetch implementation is required for authentication token requests', 'INVALID_CONFIG');
688
+ }
689
+ return globalThis.fetch(input, init);
690
+ });
618
691
  if (config.initialSubscriptions) {
619
692
  for (const subscription of config.initialSubscriptions) {
620
693
  this.addQueuedSubscription(subscription);
@@ -625,8 +698,10 @@ class ConnectionManager {
625
698
  if (this.authConfig?.tokenEndpoint) {
626
699
  return this.authConfig.tokenEndpoint;
627
700
  }
628
- // Require publishableKey for hosted token endpoint
629
- if (this.hostedAreteUrl && this.authConfig?.publishableKey) {
701
+ // Hosted stacks always mint through the Arete Cloud token endpoint.
702
+ // Without a publishable key the session is minted anonymously — the API
703
+ // rate limits by IP/origin and issues low-trust public limits instead.
704
+ if (this.hostedAreteUrl) {
630
705
  return DEFAULT_HOSTED_TOKEN_ENDPOINT;
631
706
  }
632
707
  return undefined;
@@ -671,9 +746,13 @@ class ConnectionManager {
671
746
  return scopes.every((scope) => this.tokenScopes.has(scope));
672
747
  }
673
748
  async getOrRefreshToken(forceRefresh = false, requiredScopes = ['read']) {
674
- for (const scope of requiredScopes)
749
+ const normalizedRequiredScopes = normalizeScopes(requiredScopes);
750
+ for (const scope of normalizedRequiredScopes)
675
751
  this.requestedScopes.add(scope);
676
- if (!forceRefresh && this.currentToken && !this.isTokenExpired() && this.tokenCovers(requiredScopes)) {
752
+ if (!forceRefresh
753
+ && this.currentToken
754
+ && !this.isTokenExpired()
755
+ && this.tokenCovers(normalizedRequiredScopes)) {
677
756
  return this.currentToken;
678
757
  }
679
758
  if (this.tokenRequestInFlight) {
@@ -681,18 +760,20 @@ class ConnectionManager {
681
760
  if (!this.currentToken) {
682
761
  return undefined;
683
762
  }
684
- if (this.currentToken && !this.isTokenExpired() && this.tokenCovers(requiredScopes)) {
763
+ if (this.currentToken
764
+ && !this.isTokenExpired()
765
+ && this.tokenCovers(normalizedRequiredScopes)) {
685
766
  return this.currentToken;
686
767
  }
687
- throw new AreteError(`Authentication token was not granted required scopes: ${requiredScopes.join(', ')}`, 'AUTH_REQUIRED');
768
+ throw new AreteError(`Authentication token was not granted required scopes: ${normalizedRequiredScopes.join(', ')}`, 'AUTH_REQUIRED');
688
769
  }
689
770
  let request;
690
- request = Promise.resolve().then(() => this.fetchAuthToken([...new Set([...this.tokenScopes, ...this.requestedScopes])], () => this.tokenRequestInFlight === request));
771
+ request = Promise.resolve().then(() => this.fetchAuthToken(normalizeScopes([...this.tokenScopes, ...this.requestedScopes]), () => this.tokenRequestInFlight === request));
691
772
  this.tokenRequestInFlight = request;
692
773
  try {
693
774
  const token = await request;
694
- if (token && !this.tokenCovers(requiredScopes)) {
695
- throw new AreteError(`Authentication token was not granted required scopes: ${requiredScopes.join(', ')}`, 'AUTH_REQUIRED');
775
+ if (token && !this.tokenCovers(normalizedRequiredScopes)) {
776
+ throw new AreteError(`Authentication token was not granted required scopes: ${normalizedRequiredScopes.join(', ')}`, 'AUTH_REQUIRED');
696
777
  }
697
778
  return token;
698
779
  }
@@ -703,27 +784,27 @@ class ConnectionManager {
703
784
  }
704
785
  }
705
786
  async fetchAuthToken(scopes, isCurrent) {
787
+ const request = { scopes: normalizeScopes(scopes) };
788
+ const result = await this.requestAuthToken(request, isCurrent);
789
+ return result === undefined ? undefined : this.updateTokenState(result, request.scopes);
790
+ }
791
+ async requestAuthToken(request, isCurrent) {
706
792
  const strategy = this.getAuthStrategy();
707
793
  const requireCurrent = () => {
708
794
  if (!isCurrent()) {
709
795
  throw new AreteError('Authentication request was superseded', 'CONNECTION_CANCELLED');
710
796
  }
711
797
  };
712
- // For hosted Arete URLs, auth is required - fail early with clear message
713
- if (strategy.kind === 'none' && this.hostedAreteUrl) {
714
- throw new AreteError('Arete authentication required. Please provide auth.publishableKey to AreteProvider. ' +
715
- 'Get your key from https://arete.run/dashboard', 'AUTH_REQUIRED');
716
- }
717
798
  switch (strategy.kind) {
718
799
  case 'static-token': {
719
800
  requireCurrent();
720
- return this.updateTokenState(strategy.token, scopes);
801
+ return strategy.token;
721
802
  }
722
803
  case 'token-provider':
723
804
  try {
724
- const result = await strategy.getToken({ scopes });
805
+ const result = await strategy.getToken(request);
725
806
  requireCurrent();
726
- return this.updateTokenState(result, scopes);
807
+ return result;
727
808
  }
728
809
  catch (error) {
729
810
  if (error instanceof AreteError) {
@@ -733,9 +814,9 @@ class ConnectionManager {
733
814
  }
734
815
  case 'token-endpoint':
735
816
  try {
736
- const result = await this.fetchTokenFromEndpoint(strategy.endpoint, scopes);
817
+ const result = await this.fetchTokenFromEndpoint(strategy.endpoint, request);
737
818
  requireCurrent();
738
- return this.updateTokenState(result, scopes);
819
+ return result;
739
820
  }
740
821
  catch (error) {
741
822
  if (error instanceof AreteError) {
@@ -748,14 +829,97 @@ class ConnectionManager {
748
829
  return undefined;
749
830
  }
750
831
  }
751
- createTokenEndpointRequestBody(scopes) {
832
+ updateHttpTokenState(state, result, request) {
833
+ const normalized = normalizeTokenResult(result);
834
+ if (!normalized.token) {
835
+ throw new AreteError('Authentication provider returned an empty token', 'TOKEN_INVALID');
836
+ }
837
+ const grantedScopes = new Set(normalized.scopes ?? request.scopes);
838
+ if (!request.scopes.every((scope) => grantedScopes.has(scope))) {
839
+ throw new AreteError(`Authentication token was not granted required scopes: ${request.scopes.join(', ')}`, 'AUTH_REQUIRED');
840
+ }
841
+ const expiresAt = normalizeExpiryTimestamp(normalized.expiresAt, normalized.expires_at)
842
+ ?? parseJwtExpiry(normalized.token);
843
+ if (this.isExpiryExpired(expiresAt)) {
844
+ throw new AreteError('Authentication token is expired', 'TOKEN_EXPIRED');
845
+ }
846
+ state.token = normalized.token;
847
+ state.expiresAt = expiresAt;
848
+ return normalized.token;
849
+ }
850
+ async getTargetedHttpAuthToken(request, forceRefresh) {
851
+ const identity = createTargetedHttpTokenIdentity(request);
852
+ let state = this.httpAuthTokenStates.get(identity);
853
+ if (!forceRefresh && state?.token && !this.isExpiryExpired(state.expiresAt)) {
854
+ this.touchHttpAuthTokenState(identity, state);
855
+ return state.token;
856
+ }
857
+ if (state?.inFlight) {
858
+ this.touchHttpAuthTokenState(identity, state);
859
+ return state.inFlight;
860
+ }
861
+ if (!state) {
862
+ state = {};
863
+ this.httpAuthTokenStates.set(identity, state);
864
+ }
865
+ const tokenState = state;
866
+ let inFlight;
867
+ inFlight = Promise.resolve().then(async () => {
868
+ const result = await this.requestAuthToken(request, () => this.httpAuthTokenStates.get(identity) === tokenState
869
+ && tokenState.inFlight === inFlight);
870
+ return result === undefined
871
+ ? undefined
872
+ : this.updateHttpTokenState(tokenState, result, request);
873
+ });
874
+ tokenState.inFlight = inFlight;
875
+ this.touchHttpAuthTokenState(identity, tokenState);
876
+ this.trimHttpAuthTokenStates();
877
+ try {
878
+ return await inFlight;
879
+ }
880
+ finally {
881
+ if (this.httpAuthTokenStates.get(identity) === tokenState
882
+ && tokenState.inFlight === inFlight) {
883
+ tokenState.inFlight = undefined;
884
+ }
885
+ this.trimHttpAuthTokenStates();
886
+ }
887
+ }
888
+ touchHttpAuthTokenState(identity, state) {
889
+ this.httpAuthTokenStates.delete(identity);
890
+ this.httpAuthTokenStates.set(identity, state);
891
+ }
892
+ trimHttpAuthTokenStates() {
893
+ if (this.httpAuthTokenStates.size <= MAX_HTTP_AUTH_TOKEN_STATES) {
894
+ return;
895
+ }
896
+ for (const [identity, state] of this.httpAuthTokenStates) {
897
+ if (!state.inFlight) {
898
+ this.httpAuthTokenStates.delete(identity);
899
+ }
900
+ if (this.httpAuthTokenStates.size <= MAX_HTTP_AUTH_TOKEN_STATES) {
901
+ break;
902
+ }
903
+ }
904
+ }
905
+ createTokenEndpointRequestBody(request) {
906
+ if (hasAuthTokenTarget(request)) {
907
+ const target = {
908
+ targetKind: request.targetKind,
909
+ targetId: request.targetId,
910
+ scopes: request.scopes,
911
+ };
912
+ return isProgramReadBindingAuthTarget(request)
913
+ ? { ...target, programReleaseHash: request.programReleaseHash }
914
+ : target;
915
+ }
752
916
  return {
753
917
  websocket_url: this.websocketUrl ?? '',
754
- scopes,
918
+ scopes: request.scopes,
755
919
  };
756
920
  }
757
- async fetchTokenFromEndpoint(tokenEndpoint, scopes = [...this.requestedScopes]) {
758
- const response = await fetch(tokenEndpoint, {
921
+ async fetchTokenFromEndpoint(tokenEndpoint, request) {
922
+ const response = await this.authFetch(tokenEndpoint, {
759
923
  method: 'POST',
760
924
  headers: {
761
925
  ...(this.authConfig?.publishableKey
@@ -765,7 +929,7 @@ class ConnectionManager {
765
929
  'Content-Type': 'application/json',
766
930
  },
767
931
  credentials: this.authConfig?.tokenEndpointCredentials,
768
- body: JSON.stringify(this.createTokenEndpointRequestBody(scopes)),
932
+ body: JSON.stringify(this.createTokenEndpointRequestBody(request)),
769
933
  });
770
934
  if (!response.ok) {
771
935
  const rawError = await response.text();
@@ -800,11 +964,14 @@ class ConnectionManager {
800
964
  }
801
965
  return data;
802
966
  }
803
- isTokenExpired() {
804
- if (!this.tokenExpiry) {
967
+ isExpiryExpired(expiresAt) {
968
+ if (!expiresAt) {
805
969
  return false;
806
970
  }
807
- return Date.now() >= (this.tokenExpiry - TOKEN_REFRESH_BUFFER_SECONDS) * 1000;
971
+ return Date.now() >= (expiresAt - TOKEN_REFRESH_BUFFER_SECONDS) * 1000;
972
+ }
973
+ isTokenExpired() {
974
+ return this.isExpiryExpired(this.tokenExpiry);
808
975
  }
809
976
  scheduleTokenRefresh() {
810
977
  this.clearTokenRefreshTimeout();
@@ -952,17 +1119,75 @@ class ConnectionManager {
952
1119
  getState() {
953
1120
  return this.currentState;
954
1121
  }
955
- async getHttpAuthToken(requiredScopesOrForce = ['read'], forceRefresh = false) {
956
- const requiredScopes = typeof requiredScopesOrForce === 'boolean'
957
- ? ['read']
958
- : requiredScopesOrForce;
959
- const force = typeof requiredScopesOrForce === 'boolean'
960
- ? requiredScopesOrForce
961
- : forceRefresh;
962
- return this.getOrRefreshToken(force, requiredScopes);
1122
+ async getHttpAuthToken(requestOrScopesOrTargetOrForce = ['read'], scopesTargetOrForce = false, targetOrForce = false) {
1123
+ let request;
1124
+ let force = false;
1125
+ if (typeof requestOrScopesOrTargetOrForce === 'boolean') {
1126
+ request = { scopes: ['read'] };
1127
+ force = requestOrScopesOrTargetOrForce;
1128
+ }
1129
+ else if (Array.isArray(requestOrScopesOrTargetOrForce)) {
1130
+ if (isAuthTokenTarget(scopesTargetOrForce)) {
1131
+ request = {
1132
+ scopes: requestOrScopesOrTargetOrForce,
1133
+ ...scopesTargetOrForce,
1134
+ };
1135
+ force = typeof targetOrForce === 'boolean' ? targetOrForce : false;
1136
+ }
1137
+ else if (isAuthTokenTarget(targetOrForce)) {
1138
+ request = {
1139
+ scopes: requestOrScopesOrTargetOrForce,
1140
+ ...targetOrForce,
1141
+ };
1142
+ force = typeof scopesTargetOrForce === 'boolean' ? scopesTargetOrForce : false;
1143
+ }
1144
+ else {
1145
+ request = { scopes: requestOrScopesOrTargetOrForce };
1146
+ force = typeof scopesTargetOrForce === 'boolean' ? scopesTargetOrForce : false;
1147
+ }
1148
+ }
1149
+ else if (isAuthTokenRequest(requestOrScopesOrTargetOrForce)) {
1150
+ request = requestOrScopesOrTargetOrForce;
1151
+ force = typeof scopesTargetOrForce === 'boolean' ? scopesTargetOrForce : false;
1152
+ }
1153
+ else if (isAuthTokenTarget(requestOrScopesOrTargetOrForce)) {
1154
+ const requiredScopes = Array.isArray(scopesTargetOrForce)
1155
+ ? scopesTargetOrForce
1156
+ : ['read'];
1157
+ request = {
1158
+ scopes: requiredScopes,
1159
+ ...requestOrScopesOrTargetOrForce,
1160
+ };
1161
+ force = typeof scopesTargetOrForce === 'boolean'
1162
+ ? scopesTargetOrForce
1163
+ : typeof targetOrForce === 'boolean' ? targetOrForce : false;
1164
+ }
1165
+ else {
1166
+ throw new AreteError('Invalid HTTP authentication token request', 'INVALID_CONFIG');
1167
+ }
1168
+ const normalizedRequest = normalizeAuthTokenRequest(request);
1169
+ return hasAuthTokenTarget(normalizedRequest)
1170
+ ? this.getTargetedHttpAuthToken(normalizedRequest, force)
1171
+ : this.getOrRefreshToken(force, normalizedRequest.scopes);
963
1172
  }
964
- clearHttpAuthToken() {
965
- this.clearTokenState();
1173
+ clearHttpAuthToken(identity, requiredScopes = ['read']) {
1174
+ if (identity === undefined) {
1175
+ this.clearTokenState();
1176
+ this.httpAuthTokenStates.clear();
1177
+ return;
1178
+ }
1179
+ const request = normalizeAuthTokenRequest(isAuthTokenRequest(identity)
1180
+ ? identity
1181
+ : {
1182
+ scopes: requiredScopes,
1183
+ ...identity,
1184
+ });
1185
+ if (hasAuthTokenTarget(request)) {
1186
+ this.httpAuthTokenStates.delete(createTargetedHttpTokenIdentity(request));
1187
+ }
1188
+ else {
1189
+ this.clearTokenState();
1190
+ }
966
1191
  }
967
1192
  onFrame(handler) {
968
1193
  this.frameHandlers.add(handler);
@@ -2930,7 +3155,6 @@ async function parseReadResponse(response, path) {
2930
3155
  function programAccountRead(input) {
2931
3156
  return {
2932
3157
  account: input.account,
2933
- path: input.path,
2934
3158
  schema: input.schema,
2935
3159
  };
2936
3160
  }
@@ -5029,7 +5253,7 @@ function mergeOperations(base, extension) {
5029
5253
  function extendProgram(program, extensions) {
5030
5254
  const base = program;
5031
5255
  const extended = { ...program };
5032
- delete extended.definitionHash;
5256
+ delete extended.sdkDefinitionHash;
5033
5257
  for (const key of ['pdas', 'accounts', 'queries', 'addresses', 'constants', 'defaults', 'math']) {
5034
5258
  const extensionValue = extensions[key];
5035
5259
  if (extensionValue !== undefined) {
@@ -5594,6 +5818,131 @@ function formatPreparedOperation(operation) {
5594
5818
  return lines.join('\n');
5595
5819
  }
5596
5820
 
5821
+ function appendUrl(baseUrl, path) {
5822
+ return `${baseUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
5823
+ }
5824
+ function responseErrorCode(response, body) {
5825
+ const headerCode = response.headers.get('X-Error-Code');
5826
+ if (headerCode)
5827
+ return headerCode;
5828
+ try {
5829
+ const parsed = JSON.parse(body);
5830
+ if (typeof parsed.error?.code === 'string')
5831
+ return parsed.error.code;
5832
+ return typeof parsed.code === 'string' ? parsed.code : undefined;
5833
+ }
5834
+ catch {
5835
+ return undefined;
5836
+ }
5837
+ }
5838
+ function isRefreshableErrorCode(code) {
5839
+ return shouldRefreshToken(parseErrorCode(code.trim().replace(/_/g, '-')));
5840
+ }
5841
+ function parseProgramReadResponse(response, path, body) {
5842
+ if (!response.ok) {
5843
+ throw new ReadRequestError({
5844
+ status: response.status,
5845
+ path,
5846
+ body,
5847
+ serverErrorCode: responseErrorCode(response, body),
5848
+ });
5849
+ }
5850
+ try {
5851
+ return JSON.parse(body);
5852
+ }
5853
+ catch (error) {
5854
+ throw new AreteError(`Program read '${path}' returned invalid JSON`, 'INVALID_RESPONSE', error);
5855
+ }
5856
+ }
5857
+ function hostedAuthConfig(binding, runtimeAuth) {
5858
+ const metadata = binding.auth;
5859
+ const runtimeStrategyConfigured = runtimeAuth?.token !== undefined
5860
+ || runtimeAuth?.getToken !== undefined
5861
+ || runtimeAuth?.tokenEndpoint !== undefined;
5862
+ if (runtimeStrategyConfigured)
5863
+ return runtimeAuth;
5864
+ if (metadata.required === false)
5865
+ return runtimeAuth;
5866
+ return { ...runtimeAuth, tokenEndpoint: metadata.sessionEndpoint };
5867
+ }
5868
+ function hostedAuth(config) {
5869
+ const auth = hostedAuthConfig(config.binding, config.auth);
5870
+ if (auth?.token !== undefined && !auth.token) {
5871
+ throw new AreteError('Authentication token is empty', 'TOKEN_INVALID');
5872
+ }
5873
+ const bindingId = config.binding.programReadBindingId;
5874
+ if (!auth || auth.token !== undefined) {
5875
+ return { staticToken: auth?.token };
5876
+ }
5877
+ return {
5878
+ manager: new ConnectionManager({ websocketUrl: null, auth, fetch: config.fetch }),
5879
+ target: {
5880
+ targetKind: 'program-read-binding',
5881
+ targetId: bindingId,
5882
+ programReleaseHash: config.release.programReleaseHash,
5883
+ },
5884
+ };
5885
+ }
5886
+ function requestPath(release, request) {
5887
+ const releaseHash = encodeURIComponent(release.programReleaseHash).replace(/%3A/g, ':');
5888
+ const root = `/v1/releases/${releaseHash}`
5889
+ + `/accounts/${encodeURIComponent(request.account)}`;
5890
+ if (request.operation === 'fetchMany')
5891
+ return root;
5892
+ const addressPath = `${root.replace(/\/+$/, '')}/${encodeURIComponent(request.address)}`;
5893
+ return request.operation === 'exists' ? `${addressPath}/exists` : addressPath;
5894
+ }
5895
+ function createProgramReadTransport(config) {
5896
+ if (config.kind === 'unavailable') {
5897
+ return {
5898
+ async read() {
5899
+ throw new AreteError(config.message, 'INVALID_CONFIG');
5900
+ },
5901
+ };
5902
+ }
5903
+ const auth = config.kind === 'hosted-binding' ? hostedAuth(config) : undefined;
5904
+ const endpoint = config.kind === 'hosted-binding'
5905
+ ? config.binding.endpoint
5906
+ : config.endpoint;
5907
+ return {
5908
+ async read(request) {
5909
+ const path = requestPath(config.release, request);
5910
+ const input = appendUrl(endpoint, path);
5911
+ const attempt = async (forceRefresh) => {
5912
+ const token = auth?.staticToken
5913
+ ?? (auth?.manager && auth.target
5914
+ ? await auth.manager.getHttpAuthToken(auth.target, ['read'], forceRefresh)
5915
+ : undefined);
5916
+ const headers = new Headers(request.operation === 'fetchMany'
5917
+ ? { 'content-type': 'application/json' }
5918
+ : undefined);
5919
+ if (token)
5920
+ headers.set('authorization', `Bearer ${token}`);
5921
+ return config.fetch(input, {
5922
+ method: request.operation === 'fetchMany' ? 'POST' : 'GET',
5923
+ headers,
5924
+ body: request.operation === 'fetchMany'
5925
+ ? JSON.stringify({ addresses: request.addresses })
5926
+ : undefined,
5927
+ });
5928
+ };
5929
+ let response = await attempt(false);
5930
+ let body = await response.text();
5931
+ const wireCode = responseErrorCode(response, body);
5932
+ if (response.status === 401
5933
+ && wireCode
5934
+ && isRefreshableErrorCode(wireCode)
5935
+ && auth?.manager
5936
+ && auth.target) {
5937
+ auth.manager.clearHttpAuthToken(auth.target);
5938
+ response = await attempt(true);
5939
+ body = await response.text();
5940
+ }
5941
+ return parseProgramReadResponse(response, path, body);
5942
+ },
5943
+ };
5944
+ }
5945
+
5597
5946
  class TransactionTransportError extends Error {
5598
5947
  constructor(status, body) {
5599
5948
  super(body.message);
@@ -5726,6 +6075,108 @@ function createTransactionTransport(baseUrl, authenticatedFetch) {
5726
6075
  };
5727
6076
  }
5728
6077
 
6078
+ function effectiveProgramRead(stack, name, override) {
6079
+ return override ?? stack.programReads?.[name];
6080
+ }
6081
+ function validateReleaseIdentity(programName, definition, release) {
6082
+ if (release
6083
+ && definition.programSpecHash
6084
+ && release.programSpecHash !== definition.programSpecHash) {
6085
+ throw new AreteError(`Program '${programName}' release programSpecHash '${release.programSpecHash}' does not match definition programSpecHash '${definition.programSpecHash}'`, 'PROGRAM_RELEASE_MISMATCH');
6086
+ }
6087
+ }
6088
+ function isNonEmptyString(value) {
6089
+ return typeof value === 'string' && value.trim().length > 0;
6090
+ }
6091
+ function isSecureOrLoopbackHttpUrl$1(value) {
6092
+ try {
6093
+ const url = new URL(value);
6094
+ return url.protocol === 'https:'
6095
+ || (url.protocol === 'http:'
6096
+ && ['localhost', '127.0.0.1', '::1'].includes(url.hostname));
6097
+ }
6098
+ catch {
6099
+ return false;
6100
+ }
6101
+ }
6102
+ function validateProgramReadDescriptor(programName, descriptor) {
6103
+ if (!descriptor.release
6104
+ || !isNonEmptyString(descriptor.release.programReleaseHash)
6105
+ || !isNonEmptyString(descriptor.release.programSpecHash)) {
6106
+ throw new AreteError(`Program '${programName}' read descriptor requires a complete release`, 'INVALID_CONFIG');
6107
+ }
6108
+ if (!descriptor.transport || typeof descriptor.transport !== 'object') {
6109
+ throw new AreteError(`Program '${programName}' read descriptor requires a transport`, 'INVALID_CONFIG');
6110
+ }
6111
+ if (descriptor.transport.kind === 'local-http') {
6112
+ if (descriptor.transport.endpointSource !== 'connect-http-url') {
6113
+ throw new AreteError(`Program '${programName}' local HTTP transport must use endpointSource 'connect-http-url'`, 'INVALID_CONFIG');
6114
+ }
6115
+ return;
6116
+ }
6117
+ if (descriptor.transport.kind !== 'hosted-binding') {
6118
+ throw new AreteError(`Program '${programName}' read descriptor has an unsupported transport`, 'INVALID_CONFIG');
6119
+ }
6120
+ const { binding } = descriptor.transport;
6121
+ if (!binding
6122
+ || !isSecureOrLoopbackHttpUrl$1(binding.endpoint)
6123
+ || !/^prb_[A-Za-z0-9_-]{32}$/.test(binding.programReadBindingId)
6124
+ || !binding.auth
6125
+ || binding.auth.targetKind !== 'program-read-binding'
6126
+ || binding.auth.targetId !== binding.programReadBindingId
6127
+ || !isSecureOrLoopbackHttpUrl$1(binding.auth.sessionEndpoint)) {
6128
+ throw new AreteError(`Program '${programName}' hosted binding requires secure endpoints, a canonical binding ID, and matching program-read-binding auth metadata`, 'INVALID_CONFIG');
6129
+ }
6130
+ }
6131
+ function validateProgramReads(stack, overrides, validateDescriptorKeys = true) {
6132
+ const programKeys = Object.keys(stack.programs ?? {});
6133
+ if (validateDescriptorKeys && stack.programReads) {
6134
+ const readKeys = Object.keys(stack.programReads);
6135
+ const missing = programKeys.filter((key) => !readKeys.includes(key));
6136
+ const extra = readKeys.filter((key) => !programKeys.includes(key));
6137
+ if (missing.length > 0 || extra.length > 0) {
6138
+ throw new AreteError(`Stack '${stack.name}' programReads keys must exactly match programs`
6139
+ + `${missing.length > 0 ? `; missing: ${missing.join(', ')}` : ''}`
6140
+ + `${extra.length > 0 ? `; unknown: ${extra.join(', ')}` : ''}`, 'INVALID_CONFIG');
6141
+ }
6142
+ }
6143
+ for (const [name, descriptor] of Object.entries(stack.programReads ?? {})) {
6144
+ validateProgramReadDescriptor(name, descriptor);
6145
+ }
6146
+ for (const key of Object.keys(overrides ?? {})) {
6147
+ if (!programKeys.includes(key)) {
6148
+ throw new AreteError(`Program read override '${key}' does not match a program in stack '${stack.name}'`, 'INVALID_CONFIG');
6149
+ }
6150
+ validateProgramReadDescriptor(key, overrides[key]);
6151
+ }
6152
+ for (const [name, definition] of Object.entries(stack.programs ?? {})) {
6153
+ validateReleaseIdentity(name, definition, stack.programReads?.[name]?.release);
6154
+ validateReleaseIdentity(name, definition, overrides?.[name]?.release);
6155
+ }
6156
+ }
6157
+ function hasCompleteIndependentProgramReads(stack, overrides, connectHttpUrl) {
6158
+ const programs = Object.entries(stack.programs ?? {});
6159
+ const readablePrograms = programs
6160
+ .filter(([, definition]) => hasProgramAccountReads(definition));
6161
+ return programs.length > 0 && readablePrograms.every(([name]) => {
6162
+ const read = effectiveProgramRead(stack, name, overrides?.[name]);
6163
+ return read?.transport.kind === 'hosted-binding'
6164
+ || (read?.transport.kind === 'local-http' && isNonEmptyString(connectHttpUrl));
6165
+ });
6166
+ }
6167
+ function validateLocalProgramReadEndpoints(stack, overrides, connectHttpUrl) {
6168
+ for (const [name, definition] of Object.entries(stack.programs ?? {})) {
6169
+ if (!hasProgramAccountReads(definition))
6170
+ continue;
6171
+ const descriptor = effectiveProgramRead(stack, name, overrides?.[name]);
6172
+ if (descriptor?.transport.kind === 'local-http' && !isNonEmptyString(connectHttpUrl)) {
6173
+ throw new AreteError(`Program '${name}' local HTTP transport requires ConnectOptions.httpUrl`, 'INVALID_CONFIG');
6174
+ }
6175
+ }
6176
+ }
6177
+ function hasProgramAccountReads(definition) {
6178
+ return Object.keys(definition.accounts ?? {}).length > 0;
6179
+ }
5729
6180
  function mergeAttachedPrograms(stack, attachedPrograms) {
5730
6181
  const merged = { ...(attachedPrograms ?? {}) };
5731
6182
  for (const [name, definition] of Object.entries(stack.programs ?? {})) {
@@ -5795,11 +6246,14 @@ function composeExecutionCallbacks(defaultCallback, callCallback) {
5795
6246
  };
5796
6247
  }
5797
6248
  let Arete$1 = class Arete {
5798
- constructor(url, httpBaseUrl, options) {
6249
+ constructor(url, httpBaseUrl, connectHttpUrl, programReadOverrides, options) {
5799
6250
  this.stack = options.stack;
5800
6251
  this._wallet = options.wallet;
5801
6252
  this.executionDefaults = options.execution;
5802
6253
  this.httpBaseUrl = httpBaseUrl;
6254
+ this.connectHttpUrl = connectHttpUrl;
6255
+ this.programReadOverrides = programReadOverrides;
6256
+ this.auth = options.auth;
5803
6257
  this.fetchImpl = options.fetch ?? this.resolveFetchImpl();
5804
6258
  this.storage = new SortedStorageDecorator(options.storage ?? new MemoryAdapter());
5805
6259
  this.queryStore = new QueryStore(this.storage);
@@ -5818,6 +6272,7 @@ let Arete$1 = class Arete {
5818
6272
  reconnectIntervals: options.reconnectIntervals,
5819
6273
  maxReconnectAttempts: options.maxReconnectAttempts,
5820
6274
  auth: options.auth,
6275
+ fetch: this.fetchImpl,
5821
6276
  });
5822
6277
  this.subscriptionRegistry = new SubscriptionRegistry(this.connection, this.queryStore);
5823
6278
  this.connection.onFrame((frame) => {
@@ -5828,12 +6283,12 @@ let Arete$1 = class Arete {
5828
6283
  });
5829
6284
  this._views = createTypedViews(this.stack, this.storage, this.subscriptionRegistry);
5830
6285
  this._queries = this.buildQueries();
5831
- this._chain = createChainClient(this.httpBaseUrl, ((input, init) => this.authenticatedFetch(typeof input === 'string'
6286
+ this._chain = options.chain ?? createChainClient(this.httpBaseUrl ?? '', ((input, init) => this.authenticatedStackFetch(typeof input === 'string'
5832
6287
  ? input
5833
6288
  : input instanceof URL
5834
6289
  ? input.toString()
5835
6290
  : input.url, init, ['read'])));
5836
- this._transactions = createTransactionTransport(this.httpBaseUrl, (input, init, scope, requirePreDispatchMarker) => this.authenticatedFetch(input, init, [scope], requirePreDispatchMarker));
6291
+ this._transactions = options.transactions ?? createTransactionTransport(this.httpBaseUrl ?? '', (input, init, scope, requirePreDispatchMarker) => this.authenticatedStackFetch(input, init, [scope], requirePreDispatchMarker));
5837
6292
  this._programs = this.buildPrograms();
5838
6293
  }
5839
6294
  resolveFetchImpl() {
@@ -5852,13 +6307,14 @@ let Arete$1 = class Arete {
5852
6307
  buildPrograms() {
5853
6308
  const bases = {};
5854
6309
  for (const [name, definition] of Object.entries(this.stack.programs ?? {})) {
6310
+ const transport = this.createProgramTransport(name, definition);
5855
6311
  const instructions = {};
5856
6312
  for (const [instructionName, handler] of Object.entries(definition.rawInstructions ?? {})) {
5857
6313
  instructions[instructionName] = this.createTypedInstruction(handler);
5858
6314
  }
5859
6315
  const accounts = {};
5860
6316
  for (const [accountName, accountDefinition] of Object.entries(definition.accounts ?? {})) {
5861
- accounts[accountName] = this.createAccountReader(accountDefinition);
6317
+ accounts[accountName] = this.createAccountReader(accountDefinition, transport);
5862
6318
  }
5863
6319
  const queries = {};
5864
6320
  for (const [queryName, queryDefinition] of Object.entries(definition.queries ?? {})) {
@@ -5908,21 +6364,65 @@ let Arete$1 = class Arete {
5908
6364
  build: (params, options) => buildInstruction(handler, params, this.withWallet(options)),
5909
6365
  };
5910
6366
  }
5911
- createAccountReader(definition) {
6367
+ createProgramTransport(name, definition) {
6368
+ if (!hasProgramAccountReads(definition)) {
6369
+ return createProgramReadTransport({
6370
+ kind: 'unavailable',
6371
+ message: `Program '${name}' has no generated account readers`,
6372
+ });
6373
+ }
6374
+ const descriptor = effectiveProgramRead(this.stack, name, this.programReadOverrides[name]);
6375
+ validateReleaseIdentity(name, definition, descriptor?.release);
6376
+ if (descriptor?.transport.kind === 'local-http') {
6377
+ return createProgramReadTransport({
6378
+ kind: 'local-http',
6379
+ endpoint: this.connectHttpUrl,
6380
+ release: descriptor.release,
6381
+ fetch: this.fetchImpl,
6382
+ });
6383
+ }
6384
+ if (descriptor?.transport.kind === 'hosted-binding') {
6385
+ return createProgramReadTransport({
6386
+ kind: 'hosted-binding',
6387
+ release: descriptor.release,
6388
+ binding: descriptor.transport.binding,
6389
+ auth: this.auth,
6390
+ fetch: this.fetchImpl,
6391
+ });
6392
+ }
6393
+ return createProgramReadTransport({
6394
+ kind: 'unavailable',
6395
+ message: `Program '${name}' has no release-aware read descriptor`,
6396
+ });
6397
+ }
6398
+ createAccountReader(definition, transport) {
5912
6399
  return {
5913
6400
  fetch: async (address) => {
5914
- const result = await this.readJson(`${definition.path}/${encodeURIComponent(address)}`);
6401
+ const result = await transport.read({
6402
+ operation: 'fetch',
6403
+ account: definition.account,
6404
+ address,
6405
+ });
5915
6406
  return result === null ? null : parseProgramAccountValue(definition, result);
5916
6407
  },
5917
6408
  fetchMany: async (addresses) => {
5918
- const result = await this.readJson(definition.path, {
5919
- method: 'POST',
5920
- body: { addresses },
6409
+ const result = await transport.read({
6410
+ operation: 'fetchMany',
6411
+ account: definition.account,
6412
+ addresses,
5921
6413
  });
5922
- return result.map((value) => value === null ? null : parseProgramAccountValue(definition, value));
6414
+ return {
6415
+ items: result.items.map((item) => item.status === 'ok'
6416
+ ? { ...item, value: parseProgramAccountValue(definition, item.value) }
6417
+ : item),
6418
+ };
5923
6419
  },
5924
6420
  exists: async (address) => {
5925
- const result = await this.readJson(`${definition.path}/${encodeURIComponent(address)}/exists`);
6421
+ const result = await transport.read({
6422
+ operation: 'exists',
6423
+ account: definition.account,
6424
+ address,
6425
+ });
5926
6426
  return result.exists;
5927
6427
  },
5928
6428
  };
@@ -5977,7 +6477,16 @@ let Arete$1 = class Arete {
5977
6477
  }
5978
6478
  return response;
5979
6479
  }
6480
+ authenticatedStackFetch(input, init, requiredScopes = ['read'], requirePreDispatchMarker = false) {
6481
+ if (!this.httpBaseUrl) {
6482
+ throw new AreteError(`Stack '${this.stack.name}' has no HTTP endpoint; this operation is not a program account read`, 'INVALID_CONFIG');
6483
+ }
6484
+ return this.authenticatedFetch(input, init, requiredScopes, requirePreDispatchMarker);
6485
+ }
5980
6486
  resolveReadUrl(path) {
6487
+ if (!this.httpBaseUrl) {
6488
+ throw new AreteError(`Stack '${this.stack.name}' has no HTTP endpoint; stack queries require httpUrl or endpoints.http`, 'INVALID_CONFIG');
6489
+ }
5981
6490
  return `${this.httpBaseUrl.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
5982
6491
  }
5983
6492
  /** Merge the client's default wallet into call options (call options win). */
@@ -5989,21 +6498,35 @@ let Arete$1 = class Arete {
5989
6498
  return merged;
5990
6499
  }
5991
6500
  static async connect(stack, options) {
6501
+ validateProgramReads(stack, undefined);
6502
+ const attachedPrograms = options?.programs;
6503
+ const effectiveStack = withPrograms(stack, attachedPrograms);
6504
+ const programReadOverrides = options?.programReads ?? {};
6505
+ validateProgramReads(effectiveStack, programReadOverrides, false);
5992
6506
  const requestedUrl = options?.url ?? stack.endpoints.ws;
6507
+ const connectHttpUrl = options?.httpUrl;
6508
+ validateLocalProgramReadEndpoints(effectiveStack, programReadOverrides, connectHttpUrl);
5993
6509
  const autoConnect = options?.autoConnect !== false;
5994
- const httpOnly = options?.transport === 'http';
6510
+ const independentlyReadable = hasCompleteIndependentProgramReads(effectiveStack, programReadOverrides, connectHttpUrl);
6511
+ const implicitProgramOnlyHttp = options?.transport === undefined
6512
+ && !requestedUrl
6513
+ && independentlyReadable
6514
+ && Object.keys(stack.views).length === 0;
6515
+ const httpOnly = options?.transport === 'http' || implicitProgramOnlyHttp;
5995
6516
  const url = httpOnly ? null : requestedUrl;
5996
- const httpUrl = options?.httpUrl
5997
- ?? stack.endpoints.http
5998
- ?? (requestedUrl ? deriveHttpEndpoint(requestedUrl) : undefined);
6517
+ const hasGeneratedHttp = Object.prototype.hasOwnProperty.call(stack.endpoints, 'http');
6518
+ let httpUrl = options?.httpUrl;
6519
+ if (httpUrl === undefined) {
6520
+ if (hasGeneratedHttp) {
6521
+ httpUrl = stack.endpoints.http;
6522
+ }
6523
+ }
5999
6524
  if (!httpOnly && !url) {
6000
6525
  throw new AreteError('WebSocket URL is required (provide url option or define endpoints.ws in stack)', 'INVALID_CONFIG');
6001
6526
  }
6002
- if (!httpUrl) {
6527
+ if (!httpUrl && !independentlyReadable) {
6003
6528
  throw new AreteError('HTTP endpoint is required for transport: "http" (provide httpUrl option or define endpoints.http in stack)', 'INVALID_CONFIG');
6004
6529
  }
6005
- const attachedPrograms = options?.programs;
6006
- const effectiveStack = withPrograms(stack, attachedPrograms);
6007
6530
  const internalOptions = {
6008
6531
  stack: effectiveStack,
6009
6532
  httpUrl,
@@ -6020,8 +6543,10 @@ let Arete$1 = class Arete {
6020
6543
  wallet: options?.wallet,
6021
6544
  fetch: options?.fetch,
6022
6545
  execution: options?.execution,
6546
+ chain: options?.chain,
6547
+ transactions: options?.transactions,
6023
6548
  };
6024
- const client = new Arete(url, httpUrl, internalOptions);
6549
+ const client = new Arete(url, httpUrl || undefined, connectHttpUrl, programReadOverrides, internalOptions);
6025
6550
  if (!httpOnly && autoConnect) {
6026
6551
  await client.connection.connect();
6027
6552
  }
@@ -6232,12 +6757,22 @@ function createSignerRegistry(entries = []) {
6232
6757
  };
6233
6758
  }
6234
6759
 
6760
+ function mergeProgramReadOverrides(...layers) {
6761
+ return layers.reduce((selected, layer) => layer ?? selected, undefined);
6762
+ }
6763
+ function resolveSessionProgramReads(stack, member, options) {
6764
+ const reads = Object.fromEntries(Object.keys(stack.programs ?? {}).flatMap((name) => {
6765
+ const override = mergeProgramReadOverrides(options?.programRead, options?.programReads?.[name], member?.programRead, member?.programReads?.[name]);
6766
+ return override ? [[name, override]] : [];
6767
+ }));
6768
+ return Object.keys(reads).length > 0 ? reads : undefined;
6769
+ }
6235
6770
  function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
6236
6771
  const transport = member?.transport ?? options?.transport ?? (forceHttpOnly ? 'http' : 'ws');
6237
- // Resolution order: per-member override → the stack's own endpoints →
6238
- // session-wide fallback endpoints.
6772
+ // Stack endpoints remain on the stack definition. Only explicit session/member
6773
+ // HTTP options are forwarded as ConnectOptions.httpUrl for local Program Reads.
6239
6774
  const url = member?.url ?? (stack.endpoints.ws || options?.endpoints?.ws);
6240
- const httpUrl = member?.httpUrl ?? stack.endpoints.http ?? options?.endpoints?.http;
6775
+ const httpUrl = member?.httpUrl ?? options?.endpoints?.http;
6241
6776
  return {
6242
6777
  url,
6243
6778
  httpUrl,
@@ -6248,33 +6783,87 @@ function resolveMemberConnectOptions(stack, member, options, forceHttpOnly) {
6248
6783
  autoReconnect: member?.autoReconnect,
6249
6784
  wallet: options?.wallet,
6250
6785
  fetch: options?.fetch,
6786
+ programReads: resolveSessionProgramReads(stack, member, options),
6787
+ chain: options?.chain,
6788
+ transactions: options?.transactions,
6251
6789
  };
6252
6790
  }
6253
- function programAsStack(name, program) {
6791
+ function validateCompositionProgramReads(stack, member, options) {
6792
+ const overrides = resolveSessionProgramReads(stack, member, options);
6793
+ for (const [name, program] of Object.entries(stack.programs ?? {})) {
6794
+ const descriptor = overrides?.[name] ?? stack.programReads?.[name];
6795
+ if (descriptor)
6796
+ validateProgramReadDescriptor(name, descriptor);
6797
+ if (Object.keys(program.accounts ?? {}).length === 0)
6798
+ continue;
6799
+ const binding = descriptor?.transport.kind === 'hosted-binding'
6800
+ ? descriptor.transport.binding
6801
+ : undefined;
6802
+ if (!descriptor?.release
6803
+ || !binding
6804
+ || !binding.endpoint?.trim()
6805
+ || !binding.programReadBindingId?.trim()
6806
+ || binding.auth?.targetKind !== 'program-read-binding'
6807
+ || binding.auth.targetId !== binding.programReadBindingId
6808
+ || !binding.auth.sessionEndpoint?.trim()) {
6809
+ throw new AreteError(`Composition session program '${name}' requires a complete hosted-binding descriptor or override`, 'INVALID_CONFIG');
6810
+ }
6811
+ }
6812
+ }
6813
+ function programAsStack(name, program, descriptor) {
6254
6814
  return {
6255
6815
  name,
6256
6816
  endpoints: { ws: '' },
6257
6817
  views: {},
6258
6818
  programs: { [name]: program },
6819
+ ...(descriptor ? { programReads: { [name]: descriptor } } : {}),
6259
6820
  };
6260
6821
  }
6261
- async function createSession(definition, options) {
6822
+ async function createSession(definition, ...args) {
6823
+ const options = args[0];
6262
6824
  const stackEntries = Object.entries(definition.stacks ?? {});
6263
6825
  const programEntries = Object.entries(definition.programs ?? {});
6264
6826
  if (stackEntries.length === 0 && programEntries.length === 0) {
6265
6827
  throw new AreteError('createSession requires at least one stack or program member', 'INVALID_CONFIG');
6266
6828
  }
6829
+ if (definition.mode === 'composition' && (!options?.chain || !options?.transactions)) {
6830
+ throw new AreteError('composition sessions require explicit chain and transaction transports', 'INVALID_CONFIG');
6831
+ }
6832
+ if (definition.mode === 'composition' && options?.endpoints !== undefined) {
6833
+ throw new AreteError('composition sessions require per-member live endpoints, not shared fallback endpoints', 'INVALID_CONFIG');
6834
+ }
6835
+ if (definition.programReads) {
6836
+ const programKeys = programEntries.map(([key]) => key);
6837
+ const descriptorKeys = Object.keys(definition.programReads);
6838
+ if (programKeys.some((key) => !descriptorKeys.includes(key))
6839
+ || descriptorKeys.some((key) => !programKeys.includes(key))) {
6840
+ throw new AreteError('Session definition programReads keys must exactly match standalone programs', 'INVALID_CONFIG');
6841
+ }
6842
+ }
6843
+ if (definition.mode === 'composition') {
6844
+ for (const [key, stack] of stackEntries) {
6845
+ const member = options?.stacks?.[key];
6846
+ validateCompositionProgramReads(withPrograms(stack, member?.programs), member, options);
6847
+ }
6848
+ for (const [key, program] of programEntries) {
6849
+ validateCompositionProgramReads(programAsStack(key, program, definition.programReads?.[key]), options?.programs?.[key], options);
6850
+ }
6851
+ }
6267
6852
  let wallet = options?.wallet;
6268
6853
  const signerRegistry = options?.signerRegistry ?? createSignerRegistry();
6269
6854
  const connectedStacks = await Promise.all(stackEntries.map(async ([key, stack]) => {
6270
6855
  const memberOptions = options?.stacks?.[key];
6271
6856
  const effectiveStack = withPrograms(stack, memberOptions?.programs);
6272
6857
  const connectOptions = resolveMemberConnectOptions(effectiveStack, memberOptions, options, false);
6273
- const client = await Arete$1.connect(effectiveStack, connectOptions);
6858
+ const client = await Arete$1.connect(stack, {
6859
+ ...connectOptions,
6860
+ programs: memberOptions?.programs,
6861
+ programReads: connectOptions.programReads,
6862
+ });
6274
6863
  return [key, client];
6275
6864
  }));
6276
6865
  const connectedPrograms = await Promise.all(programEntries.map(async ([key, program]) => {
6277
- const syntheticStack = programAsStack(key, program);
6866
+ const syntheticStack = programAsStack(key, program, definition.programReads?.[key]);
6278
6867
  const connectOptions = resolveMemberConnectOptions(syntheticStack, options?.programs?.[key], options, true);
6279
6868
  const client = await Arete$1.connect(syntheticStack, connectOptions);
6280
6869
  return [key, client];
@@ -6293,21 +6882,23 @@ async function createSession(definition, options) {
6293
6882
  client.programs[key],
6294
6883
  ]);
6295
6884
  const promotedPrograms = Object.fromEntries(connectedProgramEntries);
6296
- for (const [stackKey, client] of connectedStacks) {
6297
- for (const [programKey, program] of Object.entries(client.programs)) {
6298
- if (explicitProgramKeys.has(programKey)) {
6299
- continue;
6300
- }
6301
- const existingOwner = programOwners.get(programKey);
6302
- if (existingOwner) {
6303
- console.warn(`Program '${programKey}' is bundled by stacks '${existingOwner.stack}'` +
6304
- ` (${existingOwner.programId ?? 'unknown program ID'}) and '${stackKey}'` +
6305
- ` (${program.programId ?? 'unknown program ID'}); session.programs.${programKey}` +
6306
- ` uses '${existingOwner.stack}' because it was connected first`);
6307
- continue;
6885
+ if (definition.mode !== 'composition') {
6886
+ for (const [stackKey, client] of connectedStacks) {
6887
+ for (const [programKey, program] of Object.entries(client.programs)) {
6888
+ if (explicitProgramKeys.has(programKey)) {
6889
+ continue;
6890
+ }
6891
+ const existingOwner = programOwners.get(programKey);
6892
+ if (existingOwner) {
6893
+ console.warn(`Program '${programKey}' is bundled by stacks '${existingOwner.stack}'` +
6894
+ ` (${existingOwner.programId ?? 'unknown program ID'}) and '${stackKey}'` +
6895
+ ` (${program.programId ?? 'unknown program ID'}); session.programs.${programKey}` +
6896
+ ` uses '${existingOwner.stack}' because it was connected first`);
6897
+ continue;
6898
+ }
6899
+ promotedPrograms[programKey] = program;
6900
+ programOwners.set(programKey, { stack: stackKey, programId: program.programId });
6308
6901
  }
6309
- promotedPrograms[programKey] = program;
6310
- programOwners.set(programKey, { stack: stackKey, programId: program.programId });
6311
6902
  }
6312
6903
  }
6313
6904
  const programs = promotedPrograms;
@@ -6388,6 +6979,142 @@ function flowOperation(prepare) {
6388
6979
  };
6389
6980
  }
6390
6981
 
6982
+ function isSecureOrLoopbackHttpUrl(value) {
6983
+ try {
6984
+ const url = new URL(value);
6985
+ return url.protocol === 'https:'
6986
+ || (url.protocol === 'http:'
6987
+ && ['localhost', '127.0.0.1', '::1'].includes(url.hostname));
6988
+ }
6989
+ catch {
6990
+ return false;
6991
+ }
6992
+ }
6993
+ function validateBinding(capability, binding, requiredScopes) {
6994
+ const auth = binding?.auth;
6995
+ const complete = binding
6996
+ && isSecureOrLoopbackHttpUrl(binding.endpoint)
6997
+ && /^sgb_[A-Za-z0-9_-]{32}$/.test(binding.solanaGatewayBindingId)
6998
+ && binding.cluster.trim().length > 0
6999
+ && binding.region.trim().length > 0
7000
+ && auth
7001
+ && typeof auth.required === 'boolean'
7002
+ && auth.mode === binding.authPolicy
7003
+ && isSecureOrLoopbackHttpUrl(auth.sessionEndpoint)
7004
+ && isSecureOrLoopbackHttpUrl(auth.jwksUrl)
7005
+ && auth.tokenTransport === 'bearer'
7006
+ && auth.audience === 'arete:solana-gateway'
7007
+ && auth.targetKind === 'solana-gateway-binding'
7008
+ && auth.targetId === binding.solanaGatewayBindingId
7009
+ && Array.isArray(auth.scopes)
7010
+ && requiredScopes.every((scope) => auth.scopes.includes(scope))
7011
+ && Array.isArray(auth.acceptedKeyClasses)
7012
+ && typeof auth.transactionEntitlementRequired === 'boolean';
7013
+ if (!complete) {
7014
+ throw new AreteError(`Hosted Solana gateway ${capability} binding is incomplete or inconsistent`, 'INVALID_CONFIG');
7015
+ }
7016
+ }
7017
+ function resolveFetch(fetchImpl) {
7018
+ if (fetchImpl)
7019
+ return fetchImpl;
7020
+ if (typeof globalThis.fetch === 'function')
7021
+ return globalThis.fetch.bind(globalThis);
7022
+ throw new AreteError('A fetch implementation is required for hosted Solana gateway transports', 'INVALID_CONFIG');
7023
+ }
7024
+ function hasRuntimeAuthStrategy(auth) {
7025
+ return auth?.token !== undefined
7026
+ || auth?.getToken !== undefined
7027
+ || auth?.tokenEndpoint !== undefined;
7028
+ }
7029
+ function bindingAuthConfig(binding, runtimeAuth) {
7030
+ if (hasRuntimeAuthStrategy(runtimeAuth))
7031
+ return runtimeAuth;
7032
+ if (!binding.auth.required)
7033
+ return runtimeAuth;
7034
+ return { ...runtimeAuth, tokenEndpoint: binding.auth.sessionEndpoint };
7035
+ }
7036
+ function refreshableErrorCode(response) {
7037
+ const wireCode = response.headers.get('X-Error-Code');
7038
+ return wireCode !== null
7039
+ && shouldRefreshToken(parseErrorCode(wireCode.trim().replace(/_/g, '-')));
7040
+ }
7041
+ /**
7042
+ * Construct explicit hosted chain and transaction transports from generated
7043
+ * gateway descriptors. Tokens are isolated by exact binding target and scope.
7044
+ */
7045
+ function createHostedSolanaGatewayTransports(bindings, options = {}) {
7046
+ validateBinding('chain', bindings.chain, ['read']);
7047
+ validateBinding('transactions', bindings.transactions, ['transaction:inspect', 'transaction:send']);
7048
+ const fetchImpl = resolveFetch(options.fetch);
7049
+ const managers = new Map();
7050
+ const bindingAuthentication = (binding) => {
7051
+ const auth = bindingAuthConfig(binding, options.auth);
7052
+ if (auth?.token !== undefined) {
7053
+ if (!auth.token) {
7054
+ throw new AreteError('Authentication token is empty', 'TOKEN_INVALID');
7055
+ }
7056
+ return {
7057
+ staticToken: auth.token,
7058
+ target: {
7059
+ targetKind: 'solana-gateway-binding',
7060
+ targetId: binding.solanaGatewayBindingId,
7061
+ },
7062
+ };
7063
+ }
7064
+ let manager;
7065
+ if (auth && (auth.getToken || auth.tokenEndpoint)) {
7066
+ const identity = hasRuntimeAuthStrategy(options.auth)
7067
+ ? 'runtime-auth-strategy'
7068
+ : `session-endpoint:${binding.auth.sessionEndpoint}`;
7069
+ manager = managers.get(identity);
7070
+ if (!manager) {
7071
+ manager = new ConnectionManager({ websocketUrl: null, auth, fetch: fetchImpl });
7072
+ managers.set(identity, manager);
7073
+ }
7074
+ }
7075
+ return {
7076
+ manager,
7077
+ target: {
7078
+ targetKind: 'solana-gateway-binding',
7079
+ targetId: binding.solanaGatewayBindingId,
7080
+ },
7081
+ };
7082
+ };
7083
+ const authenticatedFetch = (authentication) => async (input, init, scope, requirePreDispatchMarker) => {
7084
+ const request = { ...authentication.target, scopes: [scope] };
7085
+ const attempt = async (forceRefresh) => {
7086
+ const token = authentication.staticToken
7087
+ ?? (authentication.manager
7088
+ ? await authentication.manager.getHttpAuthToken(request, forceRefresh)
7089
+ : undefined);
7090
+ const headers = new Headers(init?.headers);
7091
+ if (token)
7092
+ headers.set('authorization', `Bearer ${token}`);
7093
+ return fetchImpl(input, { ...init, headers });
7094
+ };
7095
+ let response = await attempt(false);
7096
+ const explicitlyNotDispatched = response.headers.get('X-Arete-Upstream-Attempted') === 'false';
7097
+ if (!response.ok
7098
+ && authentication.manager
7099
+ && refreshableErrorCode(response)
7100
+ && (!requirePreDispatchMarker || explicitlyNotDispatched)) {
7101
+ authentication.manager.clearHttpAuthToken(request);
7102
+ response = await attempt(true);
7103
+ }
7104
+ return response;
7105
+ };
7106
+ const chainAuthentication = bindingAuthentication(bindings.chain);
7107
+ const transactionAuthentication = bindingAuthentication(bindings.transactions);
7108
+ const chainFetch = authenticatedFetch(chainAuthentication);
7109
+ const transactionFetch = authenticatedFetch(transactionAuthentication);
7110
+ return {
7111
+ chain: createChainClient(bindings.chain.endpoint, ((input, init) => chainFetch(typeof input === 'string'
7112
+ ? input
7113
+ : input instanceof URL ? input.toString() : input.url, init, 'read', false))),
7114
+ transactions: createTransactionTransport(bindings.transactions.endpoint, (input, init, scope, requirePreDispatchMarker) => transactionFetch(input, init, scope, requirePreDispatchMarker)),
7115
+ };
7116
+ }
7117
+
6391
7118
  function chainAccountLoader(chain) {
6392
7119
  return {
6393
7120
  async getAccount(address) {
@@ -6580,6 +7307,7 @@ exports.canonicalQueryKey = canonicalQueryKey;
6580
7307
  exports.chainAccountLoader = chainAccountLoader;
6581
7308
  exports.createChainClient = createChainClient;
6582
7309
  exports.createEntityStream = createEntityStream;
7310
+ exports.createHostedSolanaGatewayTransports = createHostedSolanaGatewayTransports;
6583
7311
  exports.createInstructionExecutor = createInstructionExecutor;
6584
7312
  exports.createInstructionHandler = createInstructionHandler;
6585
7313
  exports.createPreparedFlow = createPreparedFlow;
@@ -6659,6 +7387,7 @@ exports.toRawAmount = toRawAmount;
6659
7387
  exports.transactionOperation = transactionOperation;
6660
7388
  exports.unwrapOperationExecutionError = unwrapOperationExecutionError;
6661
7389
  exports.validateAccountResolution = validateAccountResolution;
7390
+ exports.validateProgramReadDescriptor = validateProgramReadDescriptor;
6662
7391
  exports.validateSubscriptionId = validateSubscriptionId;
6663
7392
  exports.withPrograms = withPrograms;
6664
7393
  //# sourceMappingURL=index.cjs.map