@datalayer/personal-agent-protocol 0.3.2 → 0.3.3

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.
@@ -1299,7 +1299,7 @@ function buildReactorFromPlugins(pluginsInput, options = {}) {
1299
1299
  // imperative `ctx.contribute` is not knowable before it runs, and the
1300
1300
  // graph reads those from the registry instead.
1301
1301
  contributesTo: [
1302
- ...new Set((plugin.contributes ?? []).map((record2) => record2.point.id))
1302
+ ...new Set((plugin.contributes ?? []).map((record3) => record3.point.id))
1303
1303
  ]
1304
1304
  };
1305
1305
  },
@@ -1369,8 +1369,8 @@ function buildReactorFromPlugins(pluginsInput, options = {}) {
1369
1369
  return;
1370
1370
  }
1371
1371
  const ctx = buildPhaseContext(name, current);
1372
- for (const record2 of current.plugin.contributes ?? []) {
1373
- contributions.add(name, record2.point, record2.value, record2.options);
1372
+ for (const record3 of current.plugin.contributes ?? []) {
1373
+ contributions.add(name, record3.point, record3.value, record3.options);
1374
1374
  }
1375
1375
  for (const command of current.plugin.commands ?? []) {
1376
1376
  commands.add(name, command);
@@ -2057,7 +2057,7 @@ function createPapCorePlugin(options = {}) {
2057
2057
  }
2058
2058
  return definePlugin({
2059
2059
  name: "@datalayer/personal-agent-protocol-core",
2060
- version: "0.3.2",
2060
+ version: "0.3.3",
2061
2061
  contributionPoints: [
2062
2062
  DiscoveryTransports,
2063
2063
  UrlSafetyPolicies,
@@ -2685,15 +2685,15 @@ function concat(...buffers) {
2685
2685
  return buf;
2686
2686
  }
2687
2687
  var NON_ASCII = /[^\x00-\x7f]/;
2688
- function encode(string2) {
2689
- if (typeof string2 == "string" && string2.length >= 128) {
2690
- if (NON_ASCII.test(string2))
2688
+ function encode(string3) {
2689
+ if (typeof string3 == "string" && string3.length >= 128) {
2690
+ if (NON_ASCII.test(string3))
2691
2691
  throw new TypeError("non-ASCII string encountered in encode()");
2692
- return encoder.encode(string2);
2692
+ return encoder.encode(string3);
2693
2693
  }
2694
- const bytes = new Uint8Array(string2.length);
2695
- for (let i = 0; i < string2.length; i++) {
2696
- const code = string2.charCodeAt(i);
2694
+ const bytes = new Uint8Array(string3.length);
2695
+ for (let i = 0; i < string3.length; i++) {
2696
+ const code = string3.charCodeAt(i);
2697
2697
  if (code > 127)
2698
2698
  throw new TypeError("non-ASCII string encountered in encode()");
2699
2699
  bytes[i] = code;
@@ -4122,12 +4122,12 @@ function httpsUrl5(value, label) {
4122
4122
  return parsed;
4123
4123
  }
4124
4124
  function validScopes(values) {
4125
- const scopes2 = [...new Set(values)];
4125
+ const scopes3 = [...new Set(values)];
4126
4126
  const token = /^[\x21\x23-\x5b\x5d-\x7e]+$/;
4127
- if (scopes2.length === 0 || scopes2.some((scope) => !token.test(scope))) {
4127
+ if (scopes3.length === 0 || scopes3.some((scope) => !token.test(scope))) {
4128
4128
  throw new AuthorizationError("invalid_scope", "At least one valid OAuth scope is required");
4129
4129
  }
4130
- return scopes2;
4130
+ return scopes3;
4131
4131
  }
4132
4132
  async function buildDirectSignInRequest(options) {
4133
4133
  const randomBytes2 = options.randomBytes ?? defaultRandomBytes3;
@@ -4141,14 +4141,14 @@ async function buildDirectSignInRequest(options) {
4141
4141
  new Uint8Array(await cryptoApi.subtle.digest("SHA-256", new TextEncoder().encode(verifier)))
4142
4142
  );
4143
4143
  const endpoint = httpsUrl5(options.authorizationEndpoint, "authorizationEndpoint");
4144
- const scopes2 = validScopes(options.scopes);
4144
+ const scopes3 = validScopes(options.scopes);
4145
4145
  const clientId = httpsUrl5(options.clientId, "clientId").toString();
4146
4146
  const redirectUri = httpsUrl5(options.redirectUri, "redirectUri").toString();
4147
4147
  const issuer = httpsUrl5(options.issuer, "issuer").toString().replace(/\/$/, "");
4148
4148
  endpoint.searchParams.set("response_type", "code");
4149
4149
  endpoint.searchParams.set("client_id", clientId);
4150
4150
  endpoint.searchParams.set("redirect_uri", redirectUri);
4151
- endpoint.searchParams.set("scope", scopes2.join(" "));
4151
+ endpoint.searchParams.set("scope", scopes3.join(" "));
4152
4152
  endpoint.searchParams.set("state", state);
4153
4153
  endpoint.searchParams.set("code_challenge", challenge);
4154
4154
  endpoint.searchParams.set("code_challenge_method", "S256");
@@ -4158,7 +4158,7 @@ async function buildDirectSignInRequest(options) {
4158
4158
  issuer,
4159
4159
  clientId,
4160
4160
  redirectUri,
4161
- scopes: scopes2,
4161
+ scopes: scopes3,
4162
4162
  state: new SecretValue(state),
4163
4163
  codeVerifier: new SecretValue(verifier)
4164
4164
  };
@@ -4525,6 +4525,335 @@ var DirectSignInClient = class {
4525
4525
  }
4526
4526
  };
4527
4527
 
4528
+ // src/device.ts
4529
+ var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
4530
+ function httpsUrl6(value, label) {
4531
+ let parsed;
4532
+ try {
4533
+ parsed = new URL(value);
4534
+ } catch (cause) {
4535
+ throw new AuthorizationError("invalid_url", `${label} must be an HTTPS URL`, { cause });
4536
+ }
4537
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
4538
+ throw new AuthorizationError("invalid_url", `${label} must be an HTTPS URL`);
4539
+ }
4540
+ return parsed.toString();
4541
+ }
4542
+ function scopes2(values) {
4543
+ const result = [...new Set(values)];
4544
+ const token = /^[\x21\x23-\x5b\x5d-\x7e]+$/;
4545
+ if (result.length === 0 || result.some((scope) => !token.test(scope))) {
4546
+ throw new AuthorizationError("invalid_scope", "At least one valid OAuth scope is required");
4547
+ }
4548
+ return result;
4549
+ }
4550
+ function base64Url6(bytes) {
4551
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4552
+ let output = "";
4553
+ for (let index = 0; index < bytes.length; index += 3) {
4554
+ const first = bytes[index] ?? 0;
4555
+ const second = bytes[index + 1];
4556
+ const third = bytes[index + 2];
4557
+ output += alphabet[first >>> 2];
4558
+ output += alphabet[(first & 3) << 4 | (second ?? 0) >>> 4];
4559
+ if (second !== void 0) {
4560
+ output += alphabet[(second & 15) << 2 | (third ?? 0) >>> 6];
4561
+ }
4562
+ if (third !== void 0) output += alphabet[third & 63];
4563
+ }
4564
+ return output;
4565
+ }
4566
+ function opaqueId2(prefix, bytes) {
4567
+ if (bytes.byteLength < 16) {
4568
+ throw new AuthorizationError("weak_entropy", "Device Sign-In requires 128 random bits");
4569
+ }
4570
+ return `${prefix}_${base64Url6(bytes)}`;
4571
+ }
4572
+ function record2(value, label) {
4573
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4574
+ throw new AuthorizationError("invalid_device_response", `${label} must be an object`);
4575
+ }
4576
+ return value;
4577
+ }
4578
+ function string2(value, label) {
4579
+ if (typeof value !== "string" || !value) {
4580
+ throw new AuthorizationError("invalid_device_response", `${label} must be a non-empty string`);
4581
+ }
4582
+ return value;
4583
+ }
4584
+ function parseDeviceAuthorization(value) {
4585
+ const source = record2(value, "device authorization response");
4586
+ const expiresIn = source.expires_in;
4587
+ const interval = source.interval ?? 5;
4588
+ if (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > 3600) {
4589
+ throw new AuthorizationError("invalid_device_response", "expires_in must be 1..3600");
4590
+ }
4591
+ if (!Number.isInteger(interval) || interval <= 0 || interval > 300) {
4592
+ throw new AuthorizationError("invalid_device_response", "interval must be 1..300");
4593
+ }
4594
+ const complete = source.verification_uri_complete;
4595
+ if (complete !== void 0 && typeof complete !== "string") {
4596
+ throw new AuthorizationError(
4597
+ "invalid_device_response",
4598
+ "verification_uri_complete must be a string"
4599
+ );
4600
+ }
4601
+ return {
4602
+ deviceCode: new SecretValue(string2(source.device_code, "device_code")),
4603
+ userCode: string2(source.user_code, "user_code"),
4604
+ verificationUri: httpsUrl6(
4605
+ string2(source.verification_uri, "verification_uri"),
4606
+ "verification_uri"
4607
+ ),
4608
+ ...complete === void 0 ? {} : { verificationUriComplete: httpsUrl6(complete, "verification_uri_complete") },
4609
+ expiresIn,
4610
+ interval
4611
+ };
4612
+ }
4613
+ function isPending(value) {
4614
+ if (typeof value !== "object" || value === null) return false;
4615
+ const pending = value;
4616
+ return pending.kind === "pap.device-sign-in.v1" && pending.deviceCode instanceof SecretValue && pending.expiresAt instanceof Date && pending.nextPollAt instanceof Date && typeof pending.tokenEndpoint === "string" && typeof pending.interval === "number";
4617
+ }
4618
+ var DeviceSignInClient = class {
4619
+ #reactor;
4620
+ #fetch;
4621
+ #maxResponseBytes;
4622
+ constructor(options) {
4623
+ const fetcher = options.fetch ?? globalThis.fetch;
4624
+ if (!fetcher) throw new AuthorizationError("fetch_unavailable", "A fetch implementation is required");
4625
+ const limit = options.maxResponseBytes ?? 100 * 1024;
4626
+ if (!Number.isInteger(limit) || limit <= 0) throw new RangeError("maxResponseBytes must be positive");
4627
+ this.#reactor = options.reactor;
4628
+ this.#fetch = fetcher;
4629
+ this.#maxResponseBytes = limit;
4630
+ }
4631
+ async begin(options) {
4632
+ this.#validateContext(options);
4633
+ const clock = firstContribution(this.#reactor, Clocks, "clock");
4634
+ const random = firstContribution(this.#reactor, RandomSources, "random source");
4635
+ const signer = firstContribution(this.#reactor, Signers, "signer");
4636
+ const endpoint = httpsUrl6(options.deviceAuthorizationEndpoint, "deviceAuthorizationEndpoint");
4637
+ const tokenEndpoint = httpsUrl6(options.tokenEndpoint, "tokenEndpoint");
4638
+ const issuer = httpsUrl6(options.issuer, "issuer").replace(/\/$/, "");
4639
+ const clientId = httpsUrl6(options.clientId, "clientId");
4640
+ const requestedScopes = scopes2(options.scopes);
4641
+ const now = clock.now();
4642
+ const clientAssertion = await signer.sign(
4643
+ this.#claims(clientId, endpoint, now, random.bytes(16)),
4644
+ { purpose: "client_assertion" }
4645
+ );
4646
+ const response = await this.#request(
4647
+ endpoint,
4648
+ new URLSearchParams({
4649
+ scope: requestedScopes.join(" "),
4650
+ client_id: clientId,
4651
+ client_assertion_type: JWT_CLIENT_ASSERTION,
4652
+ client_assertion: clientAssertion.reveal()
4653
+ }),
4654
+ void 0,
4655
+ options.signal
4656
+ );
4657
+ const payload = await this.#decode(response, "Device authorization");
4658
+ if (!response.ok) throw this.#remoteError(response, payload, "Company refused Device Sign-In");
4659
+ const authorization = parseDeviceAuthorization(payload);
4660
+ const expiresAt = new Date(now.getTime() + authorization.expiresIn * 1e3);
4661
+ const reference = opaqueId2("device", random.bytes(16));
4662
+ const pending = {
4663
+ kind: "pap.device-sign-in.v1",
4664
+ deviceCode: authorization.deviceCode,
4665
+ tokenEndpoint,
4666
+ issuer,
4667
+ clientId,
4668
+ tenantId: options.tenantId,
4669
+ pairwiseUserId: options.pairwiseUserId,
4670
+ sessionId: options.sessionId,
4671
+ scopes: requestedScopes,
4672
+ expiresAt,
4673
+ nextPollAt: new Date(now.getTime() + authorization.interval * 1e3),
4674
+ interval: authorization.interval
4675
+ };
4676
+ const store = firstContribution(this.#reactor, AuthorizationStateStores, "authorization state store");
4677
+ await store.put(reference, pending, { expiresAt });
4678
+ return {
4679
+ reference,
4680
+ userCode: authorization.userCode,
4681
+ verificationUri: authorization.verificationUri,
4682
+ ...authorization.verificationUriComplete === void 0 ? {} : { verificationUriComplete: authorization.verificationUriComplete },
4683
+ expiresAt,
4684
+ pollInterval: authorization.interval,
4685
+ requestedScopes
4686
+ };
4687
+ }
4688
+ async poll(reference, signal) {
4689
+ const store = firstContribution(this.#reactor, AuthorizationStateStores, "authorization state store");
4690
+ const pending = await store.take(reference);
4691
+ if (!isPending(pending)) {
4692
+ throw new AuthorizationError(
4693
+ "invalid_or_expired_device_session",
4694
+ "Device Sign-In is unknown, expired, complete, or already being polled"
4695
+ );
4696
+ }
4697
+ const clock = firstContribution(this.#reactor, Clocks, "clock");
4698
+ const now = clock.now();
4699
+ if (now.getTime() >= pending.expiresAt.getTime()) {
4700
+ throw new AuthorizationError("expired_token", "Device Sign-In has expired");
4701
+ }
4702
+ if (now.getTime() < pending.nextPollAt.getTime()) {
4703
+ await store.put(reference, pending, { expiresAt: pending.expiresAt });
4704
+ return { status: "pending", retryAt: pending.nextPollAt, grantedScopes: [], missingScopes: [] };
4705
+ }
4706
+ try {
4707
+ const result = await this.#pollToken(pending, now, signal);
4708
+ if (result.response.ok) return await this.#complete(result.response, result.payload, pending);
4709
+ const source = typeof result.payload === "object" && result.payload !== null ? result.payload : {};
4710
+ const remote = "error" in source ? source.error : void 0;
4711
+ if (remote === "authorization_pending" || remote === "slow_down") {
4712
+ const interval = pending.interval + (remote === "slow_down" ? 5 : 0);
4713
+ const resumed = { ...pending, interval, nextPollAt: new Date(now.getTime() + interval * 1e3) };
4714
+ await store.put(reference, resumed, { expiresAt: pending.expiresAt });
4715
+ return { status: "pending", retryAt: resumed.nextPollAt, grantedScopes: [], missingScopes: [] };
4716
+ }
4717
+ if (remote === "access_denied" || remote === "expired_token") {
4718
+ throw new AuthorizationError(remote, "Device Sign-In ended without authorization");
4719
+ }
4720
+ await store.put(reference, pending, { expiresAt: pending.expiresAt });
4721
+ throw this.#remoteError(result.response, result.payload, "Company refused the device token request");
4722
+ } catch (cause) {
4723
+ if (cause instanceof AuthorizationError && ["access_denied", "expired_token"].includes(cause.code)) {
4724
+ throw cause;
4725
+ }
4726
+ const existing = await store.take(reference);
4727
+ if (existing === void 0) await store.put(reference, pending, { expiresAt: pending.expiresAt });
4728
+ throw cause;
4729
+ }
4730
+ }
4731
+ async #pollToken(pending, now, signal) {
4732
+ const random = firstContribution(this.#reactor, RandomSources, "random source");
4733
+ const signer = firstContribution(this.#reactor, Signers, "signer");
4734
+ const dpop = firstContribution(this.#reactor, DpopProofProviders, "DPoP proof provider");
4735
+ const assertion = await signer.sign(
4736
+ this.#claims(pending.clientId, pending.tokenEndpoint, now, random.bytes(16)),
4737
+ { purpose: "client_assertion" }
4738
+ );
4739
+ const proof = await dpop.create({
4740
+ method: "POST",
4741
+ url: pending.tokenEndpoint,
4742
+ tenantId: pending.tenantId,
4743
+ subject: pending.pairwiseUserId,
4744
+ issuer: pending.issuer
4745
+ });
4746
+ const response = await this.#request(
4747
+ pending.tokenEndpoint,
4748
+ new URLSearchParams({
4749
+ grant_type: DEVICE_CODE_GRANT,
4750
+ device_code: pending.deviceCode.reveal(),
4751
+ session_id: pending.sessionId,
4752
+ client_id: pending.clientId,
4753
+ client_assertion_type: JWT_CLIENT_ASSERTION,
4754
+ client_assertion: assertion.reveal()
4755
+ }),
4756
+ proof.reveal(),
4757
+ signal
4758
+ );
4759
+ return { response, payload: await this.#decode(response, "Device token") };
4760
+ }
4761
+ async #complete(response, payload, pending) {
4762
+ let token;
4763
+ try {
4764
+ token = parseSessionToken(payload);
4765
+ } catch (cause) {
4766
+ throw new AuthorizationError("invalid_token_response", "Device token response is malformed", {
4767
+ statusCode: response.status,
4768
+ cause
4769
+ });
4770
+ }
4771
+ if (token.tokenType !== "DPoP" || !token.signedIn || token.sessionId !== pending.sessionId) {
4772
+ throw new AuthorizationError(
4773
+ "invalid_token_response",
4774
+ "Device Sign-In must return the signed-in proof-bound Session",
4775
+ { statusCode: response.status }
4776
+ );
4777
+ }
4778
+ if (token.scopes.some((scope) => !pending.scopes.includes(scope))) {
4779
+ throw new AuthorizationError("scope_escalation", "Company returned an unrequested scope");
4780
+ }
4781
+ if (!token.accountToken) {
4782
+ throw new AuthorizationError("missing_account_token", "Device Sign-In did not return an Account Token");
4783
+ }
4784
+ const random = firstContribution(this.#reactor, RandomSources, "random source");
4785
+ const accountTokenReference = opaqueId2("account", random.bytes(16));
4786
+ const tokens = firstContribution(this.#reactor, TokenStores, "token store");
4787
+ await tokens.put(accountTokenReference, token.accountToken);
4788
+ const { accountToken: _accountToken, ...publicToken } = token;
4789
+ return {
4790
+ status: "complete",
4791
+ token: publicToken,
4792
+ accountTokenReference,
4793
+ grantedScopes: publicToken.scopes,
4794
+ missingScopes: pending.scopes.filter((scope) => !publicToken.scopes.includes(scope))
4795
+ };
4796
+ }
4797
+ #claims(clientId, audience, now, entropy2) {
4798
+ const iat = Math.floor(now.getTime() / 1e3);
4799
+ return { iss: clientId, sub: clientId, aud: audience, iat, exp: iat + 60, jti: opaqueId2("jti", entropy2) };
4800
+ }
4801
+ #validateContext(options) {
4802
+ if (!options.tenantId || options.tenantId.length > 256) throw new RangeError("Invalid tenantId");
4803
+ const identifiers = [
4804
+ ["pairwiseUserId", options.pairwiseUserId],
4805
+ ["sessionId", options.sessionId]
4806
+ ];
4807
+ for (const [label, value] of identifiers) {
4808
+ if (!/^[A-Za-z0-9_-]{1,256}$/.test(value)) {
4809
+ throw new AuthorizationError("invalid_context", `Invalid ${label}`);
4810
+ }
4811
+ }
4812
+ }
4813
+ async #request(url, body, dpop, signal) {
4814
+ try {
4815
+ return await this.#fetch(url, {
4816
+ method: "POST",
4817
+ headers: { Accept: "application/json", ...dpop === void 0 ? {} : { DPoP: dpop } },
4818
+ body,
4819
+ credentials: "omit",
4820
+ redirect: "error",
4821
+ ...signal === void 0 ? {} : { signal }
4822
+ });
4823
+ } catch (cause) {
4824
+ throw new AuthorizationError("request_failed", "Device Sign-In request failed", { cause });
4825
+ }
4826
+ }
4827
+ async #decode(response, label) {
4828
+ if (response.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json") {
4829
+ throw new AuthorizationError("invalid_content_type", `${label} endpoint must return JSON`, {
4830
+ statusCode: response.status
4831
+ });
4832
+ }
4833
+ const body = await response.text();
4834
+ if (new TextEncoder().encode(body).byteLength > this.#maxResponseBytes) {
4835
+ throw new AuthorizationError("response_too_large", `${label} response exceeds size limit`, {
4836
+ statusCode: response.status
4837
+ });
4838
+ }
4839
+ try {
4840
+ return parseJsonStrict(body);
4841
+ } catch (cause) {
4842
+ throw new AuthorizationError("invalid_json", `${label} endpoint returned invalid JSON`, {
4843
+ statusCode: response.status,
4844
+ cause
4845
+ });
4846
+ }
4847
+ }
4848
+ #remoteError(response, payload, message2) {
4849
+ const source = typeof payload === "object" && payload !== null ? payload : {};
4850
+ const remote = "error" in source ? source.error : void 0;
4851
+ return new AuthorizationError(typeof remote === "string" ? remote : "http_error", message2, {
4852
+ statusCode: response.status
4853
+ });
4854
+ }
4855
+ };
4856
+
4528
4857
  // src/discovery.ts
4529
4858
  function normalizeDomain(domain2) {
4530
4859
  const value = domain2.replace(/\.$/, "").toLowerCase();
@@ -4857,7 +5186,9 @@ export {
4857
5186
  validateDirectSignInCallback,
4858
5187
  AuthorizationCodeGrant,
4859
5188
  DirectSignInClient,
5189
+ DEVICE_CODE_GRANT,
5190
+ DeviceSignInClient,
4860
5191
  discoverCompany,
4861
5192
  AgentIdentityClient
4862
5193
  };
4863
- //# sourceMappingURL=chunk-TU6B5MQ7.js.map
5194
+ //# sourceMappingURL=chunk-JIMKKTDT.js.map