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