@attlaz/client 1.80.0 → 1.81.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/Client.js CHANGED
@@ -99,22 +99,16 @@ export class Client {
99
99
  return client;
100
100
  }
101
101
  setClientCredentials(clientId, clientSecret = '') {
102
- const options = new OAuthClientOptions(this.apiEndpoint, clientId, clientSecret);
103
- // TODO: how to know these scopes?
104
- options.scopes = ['all'];
105
- // TODO: do we need to create a new oauth client, or can we update with these credentials?
106
- this.httpClient = new OAuthClient(options);
107
- this.transport = this.httpClient;
102
+ // Store the credentials in place; the token (if any) is preserved and used while valid. When
103
+ // no valid token exists, the client mints one from these credentials on the next request.
104
+ this.httpClient.setClientCredentials(clientId, clientSecret, ['all']);
108
105
  }
109
106
  /**
110
107
  * Configure this client as a public OAuth client (no client_secret).
111
108
  * Used for SPAs and other public clients that cannot keep a secret.
112
109
  */
113
110
  setPublicClient(clientId) {
114
- const options = new OAuthClientOptions(this.apiEndpoint, clientId, null);
115
- options.scopes = ['all'];
116
- this.httpClient = new OAuthClient(options);
117
- this.transport = this.httpClient;
111
+ this.httpClient.setClientCredentials(clientId, null, ['all']);
118
112
  }
119
113
  setVersion(version) {
120
114
  this.httpClient.setVersion(version);
@@ -1,7 +1,7 @@
1
1
  export declare class OAuthClientOptions {
2
2
  apiEndpoint: string;
3
- readonly clientId: string | null;
4
- readonly clientSecret: string | null;
3
+ clientId: string | null;
4
+ clientSecret: string | null;
5
5
  accessTokenUri: string;
6
6
  authorizationUri: string;
7
7
  redirectUri: string;
@@ -7,7 +7,10 @@ export class OAuthClientOptions {
7
7
  redirectUri = 'https://example.com/auth/github/callback';
8
8
  scopes = [];
9
9
  state = '';
10
- constructor(apiEndpoint, clientId = null, clientSecret = null) {
10
+ constructor(apiEndpoint,
11
+ // Mutable so client credentials can be updated in place (see OAuthClient.setClientCredentials)
12
+ // without recreating the client and losing its token.
13
+ clientId = null, clientSecret = null) {
11
14
  this.apiEndpoint = apiEndpoint;
12
15
  this.clientId = clientId;
13
16
  this.clientSecret = clientSecret;
@@ -7,13 +7,29 @@ export declare class OAuthClient implements ITransport {
7
7
  private readonly options;
8
8
  private debug;
9
9
  private oauthClientToken;
10
- private refreshTokenPromise;
10
+ private tokenAcquisitionPromise;
11
11
  private version;
12
12
  private parseErrorHandler;
13
13
  constructor(options: OAuthClientOptions);
14
14
  authenticate(username: string, password: string): Promise<boolean>;
15
15
  authenticate(): Promise<boolean>;
16
16
  refreshToken(): Promise<void>;
17
+ /**
18
+ * Update the client credentials in place — without recreating the client or touching the current
19
+ * token. Mirrors the PHP client: token and credentials are independent. A valid token keeps being
20
+ * used; the credentials are only the fallback to mint/renew a token when none is valid.
21
+ */
22
+ setClientCredentials(clientId: string | null, clientSecret: string | null, scopes?: string[]): void;
23
+ /**
24
+ * Ensure a usable access token exists before a signed request. Token-first with credentials as a
25
+ * fallback (mirrors the PHP client's lazy authenticate()): a valid token is left untouched; an
26
+ * expired token carrying a refresh_token is refreshed; otherwise, if confidential client
27
+ * credentials are configured, a token is minted via the client_credentials grant. Single-flighted
28
+ * so concurrent requests share one acquisition.
29
+ */
30
+ private ensureAccessToken;
31
+ private acquireToken;
32
+ private static hasRefreshToken;
17
33
  private isPublicClient;
18
34
  /**
19
35
  * Performs an OAuth token request (password, client_credentials or refresh_token grant) and
@@ -9,7 +9,9 @@ export class OAuthClient {
9
9
  options;
10
10
  debug = false;
11
11
  oauthClientToken = null;
12
- refreshTokenPromise = null;
12
+ // Single-flight guard for token acquisition (initial client-credentials auth or refresh), so
13
+ // concurrent requests share one in-flight acquisition instead of each hitting the token endpoint.
14
+ tokenAcquisitionPromise = null;
13
15
  version = null;
14
16
  parseErrorHandler = null;
15
17
  constructor(options) {
@@ -93,6 +95,58 @@ export class OAuthClient {
93
95
  throw ClientError.fromError(e);
94
96
  }
95
97
  }
98
+ /**
99
+ * Update the client credentials in place — without recreating the client or touching the current
100
+ * token. Mirrors the PHP client: token and credentials are independent. A valid token keeps being
101
+ * used; the credentials are only the fallback to mint/renew a token when none is valid.
102
+ */
103
+ setClientCredentials(clientId, clientSecret, scopes = ['all']) {
104
+ this.options.clientId = clientId;
105
+ this.options.clientSecret = clientSecret;
106
+ this.options.scopes = scopes;
107
+ }
108
+ /**
109
+ * Ensure a usable access token exists before a signed request. Token-first with credentials as a
110
+ * fallback (mirrors the PHP client's lazy authenticate()): a valid token is left untouched; an
111
+ * expired token carrying a refresh_token is refreshed; otherwise, if confidential client
112
+ * credentials are configured, a token is minted via the client_credentials grant. Single-flighted
113
+ * so concurrent requests share one acquisition.
114
+ */
115
+ async ensureAccessToken() {
116
+ if (this.oauthClientToken !== null && !OAuthClientToken.isExpired(this.oauthClientToken)) {
117
+ return;
118
+ }
119
+ if (this.tokenAcquisitionPromise === null) {
120
+ this.tokenAcquisitionPromise = this.acquireToken();
121
+ try {
122
+ await this.tokenAcquisitionPromise;
123
+ }
124
+ finally {
125
+ // Always clear, even on failure, so one failed acquisition doesn't poison every later
126
+ // request with the same rejected promise.
127
+ this.tokenAcquisitionPromise = null;
128
+ }
129
+ }
130
+ else {
131
+ await this.tokenAcquisitionPromise;
132
+ }
133
+ }
134
+ async acquireToken() {
135
+ // Expired token that carries a refresh_token → refresh grant (keeps the existing session).
136
+ if (this.oauthClientToken !== null && OAuthClientToken.isExpired(this.oauthClientToken) && OAuthClient.hasRefreshToken(this.oauthClientToken)) {
137
+ await this.refreshToken();
138
+ return;
139
+ }
140
+ // No usable token (missing, or expired without a refresh_token): mint one from client
141
+ // credentials if we have them. Public clients (no secret) can't use the client_credentials
142
+ // grant, so they fall through and the caller gets a 401 — they must obtain a token another way.
143
+ if (this.options.clientId !== null && !this.isPublicClient()) {
144
+ await this.authenticate();
145
+ }
146
+ }
147
+ static hasRefreshToken(token) {
148
+ return token.refresh_token !== undefined && token.refresh_token !== null && token.refresh_token !== '';
149
+ }
96
150
  isPublicClient() {
97
151
  return this.options.clientSecret === null || this.options.clientSecret === '';
98
152
  }
@@ -141,29 +195,13 @@ export class OAuthClient {
141
195
  }
142
196
  async request(action, parameters = null, method = 'GET', signWithOauthToken = true) {
143
197
  if (signWithOauthToken) {
144
- // A single null check both guards and narrows the token type. No access token at
145
- // all is an auth failure (401), so consumers route it to the same sign-out path as
146
- // a rejected refresh.
198
+ // Token-first, credentials-as-fallback (mirrors the PHP client): use a valid token as-is,
199
+ // otherwise refresh or mint one from client credentials. Only when nothing is available do
200
+ // we 401 — the same sign-out path consumers already handle for a rejected refresh.
201
+ await this.ensureAccessToken();
147
202
  if (this.oauthClientToken === null) {
148
203
  throw new ClientError('Unable to perform request, access token not provided', HttpStatus.HTTP_UNAUTHORIZED);
149
204
  }
150
- if (OAuthClientToken.isExpired(this.oauthClientToken)) {
151
- if (this.refreshTokenPromise === null) {
152
- this.refreshTokenPromise = this.refreshToken();
153
- try {
154
- await this.refreshTokenPromise;
155
- }
156
- finally {
157
- // Always clear, even on failure, so a single failed refresh doesn't
158
- // poison every later request with the same rejected promise (e.g.
159
- // after the user re-authenticates).
160
- this.refreshTokenPromise = null;
161
- }
162
- }
163
- else {
164
- await this.refreshTokenPromise;
165
- }
166
- }
167
205
  }
168
206
  const requestData = this.createRequestData(action, parameters, method, signWithOauthToken);
169
207
  if (this.debug) {
@@ -1 +1,5 @@
1
- export type ApiRecord = Record<string, string | number | boolean | Date | null>;
1
+ import { DataValueValue } from './DataValue.js';
2
+ export type ApiRecord = Record<string, string | number | boolean | Date | {
3
+ key: string;
4
+ value: DataValueValue;
5
+ }[] | null>;
@@ -1,6 +1,7 @@
1
+ import { ApiRecord } from '../ApiRecord.js';
1
2
  export declare class CollectionRecord {
2
3
  id: string;
3
4
  collection: string;
4
5
  properties: Record<string, unknown>;
5
- static parse(raw: Record<string, unknown>): CollectionRecord;
6
+ static parse(raw: ApiRecord): CollectionRecord;
6
7
  }
@@ -1,10 +1,11 @@
1
- import { State } from '../State.js';
1
+ import { ApiRecord } from '../ApiRecord.js';
2
2
  import { DataValueValue } from '../DataValue.js';
3
+ import { State } from '../State.js';
3
4
  export declare class Configuration {
4
5
  id: string;
5
6
  path: string;
6
7
  value: DataValueValue;
7
8
  state: State;
8
9
  scope: string;
9
- static parse(rawConfig: Record<string, unknown>): Configuration;
10
+ static parse(rawConfig: ApiRecord): Configuration;
10
11
  }
@@ -1,3 +1,4 @@
1
+ import { ApiRecord } from '../ApiRecord.js';
1
2
  import { DataValueCollection } from '../DataValueCollection.js';
2
3
  import { State } from '../State.js';
3
4
  import { StateAware } from '../StateAware.js';
@@ -8,5 +9,5 @@ export declare class CodeSourceBuildStrategy implements StateAware {
8
9
  languageId: string;
9
10
  state: State;
10
11
  data: DataValueCollection;
11
- static parse(raw: any): CodeSourceBuildStrategy;
12
+ static parse(raw: ApiRecord): CodeSourceBuildStrategy;
12
13
  }
@@ -1,6 +1,7 @@
1
+ import { ApiRecord } from '../ApiRecord.js';
1
2
  import { DataValueCollection } from '../DataValueCollection.js';
2
- import { StateAware } from '../StateAware.js';
3
3
  import { State } from '../State.js';
4
+ import { StateAware } from '../StateAware.js';
4
5
  export declare class CodeSourceRunStrategy implements StateAware {
5
6
  id: string;
6
7
  name: string;
@@ -8,5 +9,5 @@ export declare class CodeSourceRunStrategy implements StateAware {
8
9
  languageId: string;
9
10
  state: State;
10
11
  data: DataValueCollection;
11
- static parse(raw: any): CodeSourceRunStrategy;
12
+ static parse(raw: ApiRecord): CodeSourceRunStrategy;
12
13
  }
@@ -1,7 +1,8 @@
1
+ import { ApiRecord } from '../ApiRecord.js';
1
2
  export declare class SourcesAccountRepository {
2
3
  key: string;
3
4
  name: string;
4
5
  url: string;
5
6
  description: string;
6
- static parse(rawSourcesAccountRepository: any): SourcesAccountRepository;
7
+ static parse(rawSourcesAccountRepository: ApiRecord): SourcesAccountRepository;
7
8
  }
@@ -1,4 +1,5 @@
1
+ import { ApiRecord } from '../ApiRecord.js';
1
2
  export declare class SourcesAccountRepositoryBranch {
2
3
  key: string;
3
- static parse(raw: any): SourcesAccountRepositoryBranch;
4
+ static parse(raw: ApiRecord): SourcesAccountRepositoryBranch;
4
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attlaz/client",
3
- "version": "1.80.0",
3
+ "version": "1.81.0",
4
4
  "description": "Javascript Client to access Attlaz API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",