@botiverse/raft-sdk 0.1.0 → 0.1.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/README.md CHANGED
@@ -38,7 +38,49 @@ const { createRaftClient } = require("@botiverse/raft-sdk");
38
38
  ```
39
39
 
40
40
  The credential must belong to the external agent sending the message and must
41
- include the `send` capability.
41
+ be a long-lived `sk_agent_*` credential with the `send` capability. Other
42
+ credential families fail before transport.
43
+
44
+ ### Persist a credential without the Raft CLI
45
+
46
+ For a long-running Node.js bot, bootstrap an already-created `sk_agent_*`
47
+ credential once, then build clients from an explicit file store:
48
+
49
+ ```ts
50
+ import {
51
+ bootstrapRaftCredential,
52
+ createFileCredentialStore,
53
+ createRaftClientFromStore,
54
+ } from "@botiverse/raft-sdk";
55
+
56
+ const store = createFileCredentialStore(
57
+ "/var/lib/raft-bot-rss-notifier/raft-credential.json",
58
+ );
59
+
60
+ // First run only. The returned identity never includes the credential bytes.
61
+ await bootstrapRaftCredential({
62
+ serverUrl: "https://api.raft.build",
63
+ credential: process.env.RAFT_AGENT_CREDENTIAL!,
64
+ store,
65
+ });
66
+
67
+ // Later runs need only the caller-selected store.
68
+ const raft = await createRaftClientFromStore({ store });
69
+ ```
70
+
71
+ The SDK validates the credential through the credential-authenticated Agent API
72
+ and requires its `send` capability before saving it. The file store requires an
73
+ absolute caller-selected path,
74
+ atomically replaces the file, writes mode `0600`, rejects broader permissions
75
+ on POSIX, and never searches CLI profiles, environment-specific Raft homes, or
76
+ the host user's home directory. Use a custom `RaftCredentialStore` when the
77
+ deployment already has a managed secret backend.
78
+
79
+ This bootstrap accepts an existing long-lived External Agent credential. It
80
+ does not run browser device-code login, mint a new credential, rotate one, or
81
+ revoke one. Once a store contains a credential, only the identical credential
82
+ may be bootstrapped again as an idempotent check. A different credential never
83
+ overwrites the store, even when it resolves to the same Server and Agent.
42
84
 
43
85
  ## API
44
86
 
@@ -58,6 +100,22 @@ Creates a client with these options:
58
100
  Invalid client configuration throws `RaftSdkConfigurationError` before a
59
101
  request is sent.
60
102
 
103
+ ### `bootstrapRaftCredential(options)`
104
+
105
+ Validates an existing External Agent credential, derives its Agent, Server,
106
+ credential, and scope metadata from the Agent API, and saves the complete
107
+ record through `options.store`. It returns only non-secret identity metadata.
108
+
109
+ ### `createFileCredentialStore(path)`
110
+
111
+ Creates the explicit Node.js file store described above. Relative paths and
112
+ unsafe stored-file permissions fail closed.
113
+
114
+ ### `createRaftClientFromStore(options)`
115
+
116
+ Loads and validates one `RaftCredentialStore` record, then creates the same
117
+ typed client returned by `createRaftClient`.
118
+
61
119
  ### `client.messages.send(request)`
62
120
 
63
121
  Sends a message. The request accepts a Raft target, message content, optional
@@ -28,8 +28,12 @@ var __webpack_require__ = {};
28
28
  var __webpack_exports__ = {};
29
29
  __webpack_require__.r(__webpack_exports__);
30
30
  __webpack_require__.d(__webpack_exports__, {
31
+ RaftCredentialError: ()=>RaftCredentialError,
31
32
  RaftSdkConfigurationError: ()=>RaftSdkConfigurationError,
32
- createRaftClient: ()=>createRaftClient
33
+ bootstrapRaftCredential: ()=>bootstrapRaftCredential,
34
+ createFileCredentialStore: ()=>createFileCredentialStore,
35
+ createRaftClient: ()=>createRaftClient,
36
+ createRaftClientFromStore: ()=>createRaftClientFromStore
33
37
  });
34
38
  var regexes_namespaceObject = {};
35
39
  __webpack_require__.r(regexes_namespaceObject);
@@ -278,7 +282,7 @@ __webpack_require__.d(classic_schemas_namespaceObject, {
278
282
  preprocess: ()=>preprocess,
279
283
  promise: ()=>promise,
280
284
  readonly: ()=>readonly,
281
- record: ()=>record,
285
+ record: ()=>schemas_record,
282
286
  refine: ()=>refine,
283
287
  set: ()=>schemas_set,
284
288
  strictObject: ()=>strictObject,
@@ -5741,7 +5745,7 @@ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def)=>{
5741
5745
  inst.keyType = def.keyType;
5742
5746
  inst.valueType = def.valueType;
5743
5747
  });
5744
- function record(keyType, valueType, params) {
5748
+ function schemas_record(keyType, valueType, params) {
5745
5749
  return new ZodRecord({
5746
5750
  type: "record",
5747
5751
  keyType,
@@ -6180,7 +6184,7 @@ function schemas_json(params) {
6180
6184
  schemas_boolean(),
6181
6185
  schemas_null(),
6182
6186
  schemas_array(jsonSchema),
6183
- record(schemas_string(), jsonSchema)
6187
+ schemas_record(schemas_string(), jsonSchema)
6184
6188
  ]));
6185
6189
  return jsonSchema;
6186
6190
  }
@@ -6563,6 +6567,7 @@ function requireServerUrl(value) {
6563
6567
  function requireAgentCredential(value) {
6564
6568
  const credential = value.trim();
6565
6569
  if (!credential) throw new RaftSdkConfigurationError("MISSING_AGENT_CREDENTIAL", "Raft Agent credential is required");
6570
+ if (!credential.startsWith("sk_agent_") || 9 === credential.length) throw new RaftSdkConfigurationError("INVALID_AGENT_CREDENTIAL", "Raft Agent credential must be a long-lived External Agent credential");
6566
6571
  return credential;
6567
6572
  }
6568
6573
  function createRaftClient(options) {
@@ -6579,11 +6584,235 @@ function createRaftClient(options) {
6579
6584
  }
6580
6585
  });
6581
6586
  }
6587
+ function currentTimeMs() {
6588
+ return Date.now();
6589
+ }
6590
+ function currentDate() {
6591
+ return new Date(currentTimeMs());
6592
+ }
6593
+ class RaftCredentialError extends Error {
6594
+ code;
6595
+ name = "RaftCredentialError";
6596
+ constructor(code, message){
6597
+ super(message), this.code = code;
6598
+ }
6599
+ }
6600
+ function isNonEmptyString(value) {
6601
+ return "string" == typeof value && value.trim().length > 0;
6602
+ }
6603
+ function parseIdentityResponse(value) {
6604
+ if (!value || "object" != typeof value || Array.isArray(value)) return null;
6605
+ const input = value;
6606
+ if (!isNonEmptyString(input.agentId) || !isNonEmptyString(input.agentName) || null !== input.agentDisplayName && "string" != typeof input.agentDisplayName || !isNonEmptyString(input.serverId) || !isNonEmptyString(input.credentialId) || !Array.isArray(input.scopes) || !input.scopes.every(isNonEmptyString)) return null;
6607
+ return {
6608
+ agentId: input.agentId,
6609
+ agentName: input.agentName,
6610
+ agentDisplayName: input.agentDisplayName,
6611
+ serverId: input.serverId,
6612
+ credentialId: input.credentialId,
6613
+ scopes: [
6614
+ ...input.scopes
6615
+ ]
6616
+ };
6617
+ }
6618
+ function parseStoredRaftCredential(value) {
6619
+ if (!value || "object" != typeof value || Array.isArray(value)) return null;
6620
+ const input = value;
6621
+ if (1 !== input.schemaVersion || !isNonEmptyString(input.serverUrl) || !isNonEmptyString(input.credential) || !isNonEmptyString(input.storedAt)) return null;
6622
+ const identity = parseIdentityResponse(input);
6623
+ if (!identity || Number.isNaN(Date.parse(input.storedAt))) return null;
6624
+ let serverUrl;
6625
+ let credential;
6626
+ try {
6627
+ serverUrl = requireServerUrl(input.serverUrl);
6628
+ credential = requireAgentCredential(input.credential);
6629
+ } catch {
6630
+ return null;
6631
+ }
6632
+ return {
6633
+ schemaVersion: 1,
6634
+ serverUrl,
6635
+ credential,
6636
+ storedAt: input.storedAt,
6637
+ ...identity
6638
+ };
6639
+ }
6640
+ async function bootstrapRaftCredential(options) {
6641
+ const serverUrl = requireServerUrl(options.serverUrl);
6642
+ const credential = requireAgentCredential(options.credential);
6643
+ const httpFetch = options.fetch ?? globalThis.fetch;
6644
+ let existing;
6645
+ try {
6646
+ existing = await options.store.load();
6647
+ } catch {
6648
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential could not be checked before saving");
6649
+ }
6650
+ if (existing) {
6651
+ const validatedExisting = parseStoredRaftCredential(existing);
6652
+ if (!validatedExisting) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential record is invalid");
6653
+ existing = validatedExisting;
6654
+ if (existing.serverUrl !== serverUrl || existing.credential !== credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
6655
+ }
6656
+ let response;
6657
+ try {
6658
+ response = await httpFetch(`${serverUrl}/internal/agent-api/`, {
6659
+ method: "GET",
6660
+ headers: {
6661
+ authorization: `Bearer ${credential}`
6662
+ }
6663
+ });
6664
+ } catch {
6665
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", "Could not reach the Raft Server to verify the Agent credential");
6666
+ }
6667
+ if (!response.ok) {
6668
+ if (401 === response.status || 403 === response.status) throw new RaftCredentialError("CREDENTIAL_REJECTED", "The Raft Server rejected the Agent credential");
6669
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", `The Raft Server could not verify the Agent credential (status ${response.status})`);
6670
+ }
6671
+ let payload;
6672
+ try {
6673
+ payload = await response.json();
6674
+ } catch {
6675
+ throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
6676
+ }
6677
+ const identity = parseIdentityResponse(payload);
6678
+ if (!identity) throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
6679
+ if (!identity.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The Raft Agent credential does not include the send capability required by this SDK");
6680
+ if (existing && (existing.serverId !== identity.serverId || existing.agentId !== identity.agentId)) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store is already bound to a different Raft Agent identity");
6681
+ if (existing) return {
6682
+ serverUrl,
6683
+ ...identity
6684
+ };
6685
+ const record = {
6686
+ schemaVersion: 1,
6687
+ serverUrl,
6688
+ credential,
6689
+ storedAt: currentDate().toISOString(),
6690
+ ...identity
6691
+ };
6692
+ try {
6693
+ await options.store.save(record);
6694
+ } catch (error) {
6695
+ if (error instanceof RaftCredentialError && "CREDENTIAL_STORE_CONFLICT" === error.code) throw error;
6696
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The verified Agent credential could not be saved");
6697
+ }
6698
+ return {
6699
+ serverUrl,
6700
+ ...identity
6701
+ };
6702
+ }
6703
+ async function createRaftClientFromStore(options) {
6704
+ let record;
6705
+ try {
6706
+ record = await options.store.load();
6707
+ } catch {
6708
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential could not be loaded");
6709
+ }
6710
+ if (!record) throw new RaftCredentialError("CREDENTIAL_NOT_FOUND", "No stored Raft Agent credential was found");
6711
+ const validated = parseStoredRaftCredential(record);
6712
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential is invalid");
6713
+ if (!validated.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The stored Raft Agent credential does not include the send capability required by this SDK");
6714
+ return createRaftClient({
6715
+ serverUrl: validated.serverUrl,
6716
+ credential: validated.credential,
6717
+ fetch: options.fetch,
6718
+ headers: options.headers,
6719
+ retry: options.retry,
6720
+ throttle: options.throttle
6721
+ });
6722
+ }
6723
+ const external_node_crypto_namespaceObject = require("node:crypto");
6724
+ const external_node_fs_namespaceObject = require("node:fs");
6725
+ const promises_namespaceObject = require("node:fs/promises");
6726
+ const external_node_path_namespaceObject = require("node:path");
6727
+ const MAX_CREDENTIAL_FILE_BYTES = 65536;
6728
+ function createFileCredentialStore(filePath) {
6729
+ if (!filePath.trim() || !(0, external_node_path_namespaceObject.isAbsolute)(filePath)) throw new RaftSdkConfigurationError("INVALID_CREDENTIAL_PATH", "Raft credential file path must be an explicit absolute path");
6730
+ return {
6731
+ load: ()=>loadFileCredential(filePath),
6732
+ async save (record) {
6733
+ const validated = parseStoredRaftCredential(record);
6734
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "Refusing to write an invalid Raft Agent credential record");
6735
+ const directory = (0, external_node_path_namespaceObject.dirname)(filePath);
6736
+ const temporaryPath = `${filePath}.${process.pid}.${(0, external_node_crypto_namespaceObject.randomUUID)()}.tmp`;
6737
+ let handle;
6738
+ try {
6739
+ await (0, promises_namespaceObject.mkdir)(directory, {
6740
+ recursive: true,
6741
+ mode: 448
6742
+ });
6743
+ const existing = await loadFileCredential(filePath);
6744
+ if (existing) return void assertSameCredential(existing, validated);
6745
+ handle = await (0, promises_namespaceObject.open)(temporaryPath, "wx", 384);
6746
+ await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`, "utf8");
6747
+ await handle.sync();
6748
+ await (0, promises_namespaceObject.chmod)(temporaryPath, 384);
6749
+ await handle.close();
6750
+ handle = void 0;
6751
+ try {
6752
+ await (0, promises_namespaceObject.link)(temporaryPath, filePath);
6753
+ } catch (error) {
6754
+ if ("EEXIST" !== error.code) throw error;
6755
+ const winner = await loadFileCredential(filePath);
6756
+ if (!winner) throw error;
6757
+ assertSameCredential(winner, validated);
6758
+ }
6759
+ await (0, promises_namespaceObject.unlink)(temporaryPath);
6760
+ } catch (error) {
6761
+ if (handle) await handle.close().catch(()=>void 0);
6762
+ await (0, promises_namespaceObject.unlink)(temporaryPath).catch(()=>void 0);
6763
+ if (error instanceof RaftCredentialError) throw error;
6764
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The Raft Agent credential could not be written atomically");
6765
+ }
6766
+ }
6767
+ };
6768
+ }
6769
+ async function loadFileCredential(filePath) {
6770
+ let initialMetadata;
6771
+ try {
6772
+ initialMetadata = await (0, promises_namespaceObject.lstat)(filePath);
6773
+ } catch (error) {
6774
+ if ("ENOENT" === error.code) return null;
6775
+ throw fileReadError();
6776
+ }
6777
+ if (!initialMetadata.isFile() || initialMetadata.isSymbolicLink() || initialMetadata.size > MAX_CREDENTIAL_FILE_BYTES) throw fileReadError();
6778
+ let parsed;
6779
+ let handle;
6780
+ try {
6781
+ const noFollow = "win32" === process.platform ? 0 : external_node_fs_namespaceObject.constants.O_NOFOLLOW;
6782
+ handle = await (0, promises_namespaceObject.open)(filePath, external_node_fs_namespaceObject.constants.O_RDONLY | noFollow);
6783
+ const openedMetadata = await handle.stat();
6784
+ if (!openedMetadata.isFile() || openedMetadata.size > MAX_CREDENTIAL_FILE_BYTES || initialMetadata.dev !== openedMetadata.dev || initialMetadata.ino !== openedMetadata.ino) throw fileReadError();
6785
+ if ("win32" !== process.platform && (63 & openedMetadata.mode) !== 0) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file permissions are too broad; expected mode 0600");
6786
+ parsed = JSON.parse(await handle.readFile("utf8"));
6787
+ } catch (error) {
6788
+ if (error instanceof RaftCredentialError) throw error;
6789
+ throw fileReadError();
6790
+ } finally{
6791
+ await handle?.close().catch(()=>void 0);
6792
+ }
6793
+ const record = parseStoredRaftCredential(parsed);
6794
+ if (!record) throw fileReadError();
6795
+ return record;
6796
+ }
6797
+ function assertSameCredential(existing, candidate) {
6798
+ if (existing.credential !== candidate.credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
6799
+ }
6800
+ function fileReadError() {
6801
+ return new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file is invalid or unreadable");
6802
+ }
6803
+ exports.RaftCredentialError = __webpack_exports__.RaftCredentialError;
6582
6804
  exports.RaftSdkConfigurationError = __webpack_exports__.RaftSdkConfigurationError;
6805
+ exports.bootstrapRaftCredential = __webpack_exports__.bootstrapRaftCredential;
6806
+ exports.createFileCredentialStore = __webpack_exports__.createFileCredentialStore;
6583
6807
  exports.createRaftClient = __webpack_exports__.createRaftClient;
6808
+ exports.createRaftClientFromStore = __webpack_exports__.createRaftClientFromStore;
6584
6809
  for(var __rspack_i in __webpack_exports__)if (-1 === [
6810
+ "RaftCredentialError",
6585
6811
  "RaftSdkConfigurationError",
6586
- "createRaftClient"
6812
+ "bootstrapRaftCredential",
6813
+ "createFileCredentialStore",
6814
+ "createRaftClient",
6815
+ "createRaftClientFromStore"
6587
6816
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
6588
6817
  Object.defineProperty(exports, '__esModule', {
6589
6818
  value: true
package/dist/esm/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { chmod, link as promises_link, lstat, mkdir, open as promises_open, unlink } from "node:fs/promises";
4
+ import { dirname, isAbsolute } from "node:path";
1
5
  var __webpack_require__ = {};
2
6
  (()=>{
3
7
  __webpack_require__.d = (exports, getters, values)=>{
@@ -209,7 +213,7 @@ __webpack_require__.d(classic_schemas_namespaceObject, {
209
213
  preprocess: ()=>preprocess,
210
214
  promise: ()=>promise,
211
215
  readonly: ()=>readonly,
212
- record: ()=>record,
216
+ record: ()=>schemas_record,
213
217
  refine: ()=>refine,
214
218
  set: ()=>schemas_set,
215
219
  strictObject: ()=>strictObject,
@@ -5734,7 +5738,7 @@ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def)=>{
5734
5738
  inst.keyType = def.keyType;
5735
5739
  inst.valueType = def.valueType;
5736
5740
  });
5737
- function record(keyType, valueType, params) {
5741
+ function schemas_record(keyType, valueType, params) {
5738
5742
  return new ZodRecord({
5739
5743
  type: "record",
5740
5744
  keyType,
@@ -6173,7 +6177,7 @@ function schemas_json(params) {
6173
6177
  schemas_boolean(),
6174
6178
  schemas_null(),
6175
6179
  schemas_array(jsonSchema),
6176
- record(schemas_string(), jsonSchema)
6180
+ schemas_record(schemas_string(), jsonSchema)
6177
6181
  ]));
6178
6182
  return jsonSchema;
6179
6183
  }
@@ -6556,6 +6560,7 @@ function requireServerUrl(value) {
6556
6560
  function requireAgentCredential(value) {
6557
6561
  const credential = value.trim();
6558
6562
  if (!credential) throw new RaftSdkConfigurationError("MISSING_AGENT_CREDENTIAL", "Raft Agent credential is required");
6563
+ if (!credential.startsWith("sk_agent_") || 9 === credential.length) throw new RaftSdkConfigurationError("INVALID_AGENT_CREDENTIAL", "Raft Agent credential must be a long-lived External Agent credential");
6559
6564
  return credential;
6560
6565
  }
6561
6566
  function createRaftClient(options) {
@@ -6572,4 +6577,216 @@ function createRaftClient(options) {
6572
6577
  }
6573
6578
  });
6574
6579
  }
6575
- export { RaftSdkConfigurationError, createRaftClient };
6580
+ function currentTimeMs() {
6581
+ return Date.now();
6582
+ }
6583
+ function currentDate() {
6584
+ return new Date(currentTimeMs());
6585
+ }
6586
+ class RaftCredentialError extends Error {
6587
+ code;
6588
+ name = "RaftCredentialError";
6589
+ constructor(code, message){
6590
+ super(message), this.code = code;
6591
+ }
6592
+ }
6593
+ function isNonEmptyString(value) {
6594
+ return "string" == typeof value && value.trim().length > 0;
6595
+ }
6596
+ function parseIdentityResponse(value) {
6597
+ if (!value || "object" != typeof value || Array.isArray(value)) return null;
6598
+ const input = value;
6599
+ if (!isNonEmptyString(input.agentId) || !isNonEmptyString(input.agentName) || null !== input.agentDisplayName && "string" != typeof input.agentDisplayName || !isNonEmptyString(input.serverId) || !isNonEmptyString(input.credentialId) || !Array.isArray(input.scopes) || !input.scopes.every(isNonEmptyString)) return null;
6600
+ return {
6601
+ agentId: input.agentId,
6602
+ agentName: input.agentName,
6603
+ agentDisplayName: input.agentDisplayName,
6604
+ serverId: input.serverId,
6605
+ credentialId: input.credentialId,
6606
+ scopes: [
6607
+ ...input.scopes
6608
+ ]
6609
+ };
6610
+ }
6611
+ function parseStoredRaftCredential(value) {
6612
+ if (!value || "object" != typeof value || Array.isArray(value)) return null;
6613
+ const input = value;
6614
+ if (1 !== input.schemaVersion || !isNonEmptyString(input.serverUrl) || !isNonEmptyString(input.credential) || !isNonEmptyString(input.storedAt)) return null;
6615
+ const identity = parseIdentityResponse(input);
6616
+ if (!identity || Number.isNaN(Date.parse(input.storedAt))) return null;
6617
+ let serverUrl;
6618
+ let credential;
6619
+ try {
6620
+ serverUrl = requireServerUrl(input.serverUrl);
6621
+ credential = requireAgentCredential(input.credential);
6622
+ } catch {
6623
+ return null;
6624
+ }
6625
+ return {
6626
+ schemaVersion: 1,
6627
+ serverUrl,
6628
+ credential,
6629
+ storedAt: input.storedAt,
6630
+ ...identity
6631
+ };
6632
+ }
6633
+ async function bootstrapRaftCredential(options) {
6634
+ const serverUrl = requireServerUrl(options.serverUrl);
6635
+ const credential = requireAgentCredential(options.credential);
6636
+ const httpFetch = options.fetch ?? globalThis.fetch;
6637
+ let existing;
6638
+ try {
6639
+ existing = await options.store.load();
6640
+ } catch {
6641
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential could not be checked before saving");
6642
+ }
6643
+ if (existing) {
6644
+ const validatedExisting = parseStoredRaftCredential(existing);
6645
+ if (!validatedExisting) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The existing Agent credential record is invalid");
6646
+ existing = validatedExisting;
6647
+ if (existing.serverUrl !== serverUrl || existing.credential !== credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
6648
+ }
6649
+ let response;
6650
+ try {
6651
+ response = await httpFetch(`${serverUrl}/internal/agent-api/`, {
6652
+ method: "GET",
6653
+ headers: {
6654
+ authorization: `Bearer ${credential}`
6655
+ }
6656
+ });
6657
+ } catch {
6658
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", "Could not reach the Raft Server to verify the Agent credential");
6659
+ }
6660
+ if (!response.ok) {
6661
+ if (401 === response.status || 403 === response.status) throw new RaftCredentialError("CREDENTIAL_REJECTED", "The Raft Server rejected the Agent credential");
6662
+ throw new RaftCredentialError("CREDENTIAL_VERIFY_FAILED", `The Raft Server could not verify the Agent credential (status ${response.status})`);
6663
+ }
6664
+ let payload;
6665
+ try {
6666
+ payload = await response.json();
6667
+ } catch {
6668
+ throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
6669
+ }
6670
+ const identity = parseIdentityResponse(payload);
6671
+ if (!identity) throw new RaftCredentialError("CREDENTIAL_RESPONSE_INVALID", "The Raft Server returned an invalid Agent identity response");
6672
+ if (!identity.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The Raft Agent credential does not include the send capability required by this SDK");
6673
+ if (existing && (existing.serverId !== identity.serverId || existing.agentId !== identity.agentId)) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store is already bound to a different Raft Agent identity");
6674
+ if (existing) return {
6675
+ serverUrl,
6676
+ ...identity
6677
+ };
6678
+ const record = {
6679
+ schemaVersion: 1,
6680
+ serverUrl,
6681
+ credential,
6682
+ storedAt: currentDate().toISOString(),
6683
+ ...identity
6684
+ };
6685
+ try {
6686
+ await options.store.save(record);
6687
+ } catch (error) {
6688
+ if (error instanceof RaftCredentialError && "CREDENTIAL_STORE_CONFLICT" === error.code) throw error;
6689
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The verified Agent credential could not be saved");
6690
+ }
6691
+ return {
6692
+ serverUrl,
6693
+ ...identity
6694
+ };
6695
+ }
6696
+ async function createRaftClientFromStore(options) {
6697
+ let record;
6698
+ try {
6699
+ record = await options.store.load();
6700
+ } catch {
6701
+ throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential could not be loaded");
6702
+ }
6703
+ if (!record) throw new RaftCredentialError("CREDENTIAL_NOT_FOUND", "No stored Raft Agent credential was found");
6704
+ const validated = parseStoredRaftCredential(record);
6705
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The stored Agent credential is invalid");
6706
+ if (!validated.scopes.includes("send")) throw new RaftCredentialError("CREDENTIAL_SCOPE_MISSING", "The stored Raft Agent credential does not include the send capability required by this SDK");
6707
+ return createRaftClient({
6708
+ serverUrl: validated.serverUrl,
6709
+ credential: validated.credential,
6710
+ fetch: options.fetch,
6711
+ headers: options.headers,
6712
+ retry: options.retry,
6713
+ throttle: options.throttle
6714
+ });
6715
+ }
6716
+ const MAX_CREDENTIAL_FILE_BYTES = 65536;
6717
+ function createFileCredentialStore(filePath) {
6718
+ if (!filePath.trim() || !isAbsolute(filePath)) throw new RaftSdkConfigurationError("INVALID_CREDENTIAL_PATH", "Raft credential file path must be an explicit absolute path");
6719
+ return {
6720
+ load: ()=>loadFileCredential(filePath),
6721
+ async save (record) {
6722
+ const validated = parseStoredRaftCredential(record);
6723
+ if (!validated) throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "Refusing to write an invalid Raft Agent credential record");
6724
+ const directory = dirname(filePath);
6725
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
6726
+ let handle;
6727
+ try {
6728
+ await mkdir(directory, {
6729
+ recursive: true,
6730
+ mode: 448
6731
+ });
6732
+ const existing = await loadFileCredential(filePath);
6733
+ if (existing) return void assertSameCredential(existing, validated);
6734
+ handle = await promises_open(temporaryPath, "wx", 384);
6735
+ await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`, "utf8");
6736
+ await handle.sync();
6737
+ await chmod(temporaryPath, 384);
6738
+ await handle.close();
6739
+ handle = void 0;
6740
+ try {
6741
+ await promises_link(temporaryPath, filePath);
6742
+ } catch (error) {
6743
+ if ("EEXIST" !== error.code) throw error;
6744
+ const winner = await loadFileCredential(filePath);
6745
+ if (!winner) throw error;
6746
+ assertSameCredential(winner, validated);
6747
+ }
6748
+ await unlink(temporaryPath);
6749
+ } catch (error) {
6750
+ if (handle) await handle.close().catch(()=>void 0);
6751
+ await unlink(temporaryPath).catch(()=>void 0);
6752
+ if (error instanceof RaftCredentialError) throw error;
6753
+ throw new RaftCredentialError("CREDENTIAL_STORE_WRITE_FAILED", "The Raft Agent credential could not be written atomically");
6754
+ }
6755
+ }
6756
+ };
6757
+ }
6758
+ async function loadFileCredential(filePath) {
6759
+ let initialMetadata;
6760
+ try {
6761
+ initialMetadata = await lstat(filePath);
6762
+ } catch (error) {
6763
+ if ("ENOENT" === error.code) return null;
6764
+ throw fileReadError();
6765
+ }
6766
+ if (!initialMetadata.isFile() || initialMetadata.isSymbolicLink() || initialMetadata.size > MAX_CREDENTIAL_FILE_BYTES) throw fileReadError();
6767
+ let parsed;
6768
+ let handle;
6769
+ try {
6770
+ const noFollow = "win32" === process.platform ? 0 : constants.O_NOFOLLOW;
6771
+ handle = await promises_open(filePath, constants.O_RDONLY | noFollow);
6772
+ const openedMetadata = await handle.stat();
6773
+ if (!openedMetadata.isFile() || openedMetadata.size > MAX_CREDENTIAL_FILE_BYTES || initialMetadata.dev !== openedMetadata.dev || initialMetadata.ino !== openedMetadata.ino) throw fileReadError();
6774
+ if ("win32" !== process.platform && (63 & openedMetadata.mode) !== 0) throw new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file permissions are too broad; expected mode 0600");
6775
+ parsed = JSON.parse(await handle.readFile("utf8"));
6776
+ } catch (error) {
6777
+ if (error instanceof RaftCredentialError) throw error;
6778
+ throw fileReadError();
6779
+ } finally{
6780
+ await handle?.close().catch(()=>void 0);
6781
+ }
6782
+ const record = parseStoredRaftCredential(parsed);
6783
+ if (!record) throw fileReadError();
6784
+ return record;
6785
+ }
6786
+ function assertSameCredential(existing, candidate) {
6787
+ if (existing.credential !== candidate.credential) throw new RaftCredentialError("CREDENTIAL_STORE_CONFLICT", "The credential store already contains a different Raft Agent credential");
6788
+ }
6789
+ function fileReadError() {
6790
+ return new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file is invalid or unreadable");
6791
+ }
6792
+ export { RaftCredentialError, RaftSdkConfigurationError, bootstrapRaftCredential, createFileCredentialStore, createRaftClient, createRaftClientFromStore };
package/dist/index.d.ts CHANGED
@@ -178,6 +178,32 @@ declare interface AgentApiSendSentResponse {
178
178
  [key: string]: unknown;
179
179
  }
180
180
 
181
+ /**
182
+ * Validate an existing External Agent credential against Raft, derive its
183
+ * bound identity from the credential-authenticated `whoami` surface, and save
184
+ * it through the caller's explicit store. This does not mint or rotate a
185
+ * credential and never returns the credential bytes.
186
+ */
187
+ export declare function bootstrapRaftCredential(options: BootstrapRaftCredentialOptions): Promise<RaftCredentialIdentity>;
188
+
189
+ export declare interface BootstrapRaftCredentialOptions {
190
+ /** Raft Server origin whose Agent API will validate the credential. */
191
+ serverUrl: string;
192
+ /** Existing long-lived External Agent credential (`sk_agent_*`). */
193
+ credential: string;
194
+ /** Caller-selected durable store. */
195
+ store: RaftCredentialStore;
196
+ /** Optional fetch implementation for runtimes, tests, or network policy wrappers. */
197
+ fetch?: typeof fetch;
198
+ }
199
+
200
+ /**
201
+ * Create a Node.js credential store at one explicit absolute path. The SDK
202
+ * never discovers CLI profiles or a home directory. Writes use a same-directory
203
+ * temporary file followed by atomic rename, and the final file is mode 0600.
204
+ */
205
+ export declare function createFileCredentialStore(filePath: string): RaftCredentialStore;
206
+
181
207
  /**
182
208
  * Create a typed client for the credential-derived `/internal/agent-api/*`
183
209
  * surface. The SDK does not read CLI profiles, environment variables, or the
@@ -185,6 +211,17 @@ declare interface AgentApiSendSentResponse {
185
211
  */
186
212
  export declare function createRaftClient(options: CreateRaftClientOptions): RaftClient;
187
213
 
214
+ /** Load the caller-selected credential store and create a normal Raft client. */
215
+ export declare function createRaftClientFromStore(options: CreateRaftClientFromStoreOptions): Promise<RaftClient>;
216
+
217
+ export declare interface CreateRaftClientFromStoreOptions {
218
+ store: RaftCredentialStore;
219
+ fetch?: CreateRaftClientOptions["fetch"];
220
+ headers?: CreateRaftClientOptions["headers"];
221
+ retry?: CreateRaftClientOptions["retry"];
222
+ throttle?: CreateRaftClientOptions["throttle"];
223
+ }
224
+
188
225
  export declare interface CreateRaftClientOptions {
189
226
  /** Raft Server origin, for example `https://api.raft.build`. */
190
227
  serverUrl: string;
@@ -210,12 +247,45 @@ export declare type RaftClientResult = AgentApiMessageClientResult;
210
247
 
211
248
  export declare type RaftClientSuccess = AgentApiMessageClientSuccess;
212
249
 
250
+ export declare class RaftCredentialError extends Error {
251
+ readonly code: RaftCredentialErrorCode;
252
+ readonly name = "RaftCredentialError";
253
+ constructor(code: RaftCredentialErrorCode, message: string);
254
+ }
255
+
256
+ export declare type RaftCredentialErrorCode = "CREDENTIAL_REJECTED" | "CREDENTIAL_VERIFY_FAILED" | "CREDENTIAL_RESPONSE_INVALID" | "CREDENTIAL_SCOPE_MISSING" | "CREDENTIAL_STORE_READ_FAILED" | "CREDENTIAL_STORE_WRITE_FAILED" | "CREDENTIAL_STORE_CONFLICT" | "CREDENTIAL_NOT_FOUND";
257
+
258
+ export declare interface RaftCredentialIdentity {
259
+ serverUrl: string;
260
+ agentId: string;
261
+ agentName: string;
262
+ agentDisplayName: string | null;
263
+ serverId: string;
264
+ credentialId: string;
265
+ scopes: string[];
266
+ }
267
+
268
+ export declare interface RaftCredentialStore {
269
+ load(): Promise<StoredRaftCredential | null>;
270
+ /**
271
+ * Persist the first credential. Implementations must reject replacement by
272
+ * a different credential; saving the identical credential may be a no-op.
273
+ */
274
+ save(record: StoredRaftCredential): Promise<void>;
275
+ }
276
+
213
277
  export declare class RaftSdkConfigurationError extends Error {
214
278
  readonly code: RaftSdkConfigurationErrorCode;
215
279
  readonly name = "RaftSdkConfigurationError";
216
280
  constructor(code: RaftSdkConfigurationErrorCode, message: string);
217
281
  }
218
282
 
219
- export declare type RaftSdkConfigurationErrorCode = "MISSING_SERVER_URL" | "INVALID_SERVER_URL" | "MISSING_AGENT_CREDENTIAL";
283
+ export declare type RaftSdkConfigurationErrorCode = "MISSING_SERVER_URL" | "INVALID_SERVER_URL" | "MISSING_AGENT_CREDENTIAL" | "INVALID_AGENT_CREDENTIAL" | "INVALID_CREDENTIAL_PATH";
284
+
285
+ export declare interface StoredRaftCredential extends RaftCredentialIdentity {
286
+ schemaVersion: 1;
287
+ credential: string;
288
+ storedAt: string;
289
+ }
220
290
 
221
291
  export { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Typed Raft Agent API client for external agents and bots.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",