toolcraft 0.0.50 → 0.0.52

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.
Files changed (47) hide show
  1. package/node_modules/@poe-code/agent-defs/README.md +35 -0
  2. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +9 -2
  3. package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +3 -1
  4. package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +44 -2
  5. package/node_modules/@poe-code/agent-human-in-loop/package.json +0 -1
  6. package/node_modules/@poe-code/agent-mcp-config/README.md +54 -0
  7. package/node_modules/@poe-code/agent-mcp-config/package.json +0 -2
  8. package/node_modules/@poe-code/config-mutations/README.md +55 -0
  9. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +1 -0
  10. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +65 -0
  11. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +4 -11
  12. package/node_modules/@poe-code/config-mutations/package.json +0 -1
  13. package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +17 -0
  14. package/node_modules/@poe-code/frontmatter/dist/fences.js +33 -5
  15. package/node_modules/@poe-code/frontmatter/dist/parse.js +86 -14
  16. package/node_modules/@poe-code/frontmatter/dist/stringify.js +13 -0
  17. package/node_modules/@poe-code/process-runner/dist/docker/args.js +14 -1
  18. package/node_modules/@poe-code/process-runner/dist/docker/build-context.d.ts +5 -0
  19. package/node_modules/@poe-code/process-runner/dist/docker/build-context.js +37 -0
  20. package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +29 -29
  21. package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
  22. package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
  23. package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +49 -3
  24. package/node_modules/@poe-code/process-runner/package.json +8 -1
  25. package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +7 -0
  26. package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +34 -7
  27. package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +75 -19
  28. package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
  29. package/node_modules/@poe-code/task-list/dist/backends/utils.js +23 -2
  30. package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +16 -12
  31. package/node_modules/@poe-code/task-list/dist/state-machine.js +9 -0
  32. package/node_modules/@poe-code/task-list/package.json +0 -1
  33. package/node_modules/auth-store/dist/create-secret-store.js +4 -3
  34. package/node_modules/auth-store/dist/encrypted-file-store.js +49 -2
  35. package/node_modules/auth-store/dist/keychain-store.js +11 -4
  36. package/node_modules/auth-store/package.json +0 -1
  37. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +69 -12
  38. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +100 -68
  39. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +19 -18
  40. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +37 -31
  41. package/node_modules/mcp-oauth/package.json +0 -1
  42. package/node_modules/tiny-mcp-client/dist/internal.js +96 -10
  43. package/node_modules/tiny-mcp-client/package.json +0 -3
  44. package/node_modules/tiny-mcp-client/src/internal.ts +120 -18
  45. package/node_modules/tiny-mcp-client/src/transports.test.ts +231 -2
  46. package/node_modules/toolcraft-design/package.json +0 -1
  47. package/package.json +3 -2
@@ -36,8 +36,20 @@ export function sortStrings(values) {
36
36
  export function sortTasks(tasks) {
37
37
  return [...tasks].sort((left, right) => left.qualifiedId.localeCompare(right.qualifiedId));
38
38
  }
39
+ export function isTrimmedPrintableIdentifier(value) {
40
+ if (value.length === 0 || value !== value.trim()) {
41
+ return false;
42
+ }
43
+ for (let index = 0; index < value.length; index += 1) {
44
+ const code = value.charCodeAt(index);
45
+ if (code < 32 || code === 127) {
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
39
51
  export function validateTaskId(id) {
40
- if (id.length === 0 ||
52
+ if (!isTrimmedPrintableIdentifier(id) ||
41
53
  id.startsWith(".") ||
42
54
  id.includes("/") ||
43
55
  id.includes("\\") ||
@@ -46,6 +58,12 @@ export function validateTaskId(id) {
46
58
  }
47
59
  return id;
48
60
  }
61
+ export function validateTaskName(name) {
62
+ if (name.trim().length === 0) {
63
+ throw new Error("Task name must not be empty.");
64
+ }
65
+ return name;
66
+ }
49
67
  export async function statIfExists(fs, filePath) {
50
68
  try {
51
69
  return await fs.stat(filePath);
@@ -66,6 +84,9 @@ export async function rejectSymbolicLinkComponents(fs, filePath) {
66
84
  currentPath = path.join(currentPath, component);
67
85
  try {
68
86
  if ((await fs.lstat(currentPath)).isSymbolicLink()) {
87
+ if (currentPath === "/tmp") {
88
+ continue;
89
+ }
69
90
  throw new Error(`Path "${filePath}" contains a symbolic link.`);
70
91
  }
71
92
  }
@@ -138,7 +159,7 @@ async function removeAbandonedLock(fs, lockPath) {
138
159
  throw error;
139
160
  }
140
161
  const owner = Number(content);
141
- if (!Number.isInteger(owner) || owner <= 0 || isProcessRunning(owner)) {
162
+ if (Number.isInteger(owner) && owner > 0 && isProcessRunning(owner)) {
142
163
  return false;
143
164
  }
144
165
  try {
@@ -5,7 +5,7 @@ import taskSchema from "../schema/task.schema.json" with { type: "json" };
5
5
  import { eventsFromState, findEvent } from "../state-machine.js";
6
6
  import { resolveStateMachine } from "../state.js";
7
7
  import { AnchorNotFoundError, InvalidTransitionError, MalformedTaskError, OrderMismatchError, TaskAlreadyExistsError, TaskNotFoundError } from "../types.js";
8
- import { applyOrder, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, withFileLock, writeAtomically } from "./utils.js";
8
+ import { applyOrder, isTrimmedPrintableIdentifier, isRecord, rejectSymbolicLinkComponents, sortStrings, statIfExists, validateTaskId, validateTaskName, withFileLock, writeAtomically } from "./utils.js";
9
9
  const STORE_KIND = "task-store";
10
10
  const STORE_SCHEMA_ID = storeSchema.$id;
11
11
  const STORE_VERSION = 1;
@@ -28,7 +28,7 @@ function malformedTask(list, id, field) {
28
28
  return new MalformedTaskError(`Malformed task "${list}/${id}": invalid "${field}".`);
29
29
  }
30
30
  function validateListName(name) {
31
- if (name.length === 0 ||
31
+ if (!isTrimmedPrintableIdentifier(name) ||
32
32
  name.startsWith(".") ||
33
33
  name.includes("/") ||
34
34
  name.includes("\\") ||
@@ -171,9 +171,7 @@ function assertValidStoreRecord(store, filePath) {
171
171
  throw malformedStore(filePath, "kind");
172
172
  }
173
173
  const version = getOwnEntry(store, "version");
174
- if (typeof version !== "number" ||
175
- !Number.isInteger(version) ||
176
- version !== STORE_VERSION) {
174
+ if (typeof version !== "number" || !Number.isInteger(version) || version !== STORE_VERSION) {
177
175
  throw malformedStore(filePath, "version");
178
176
  }
179
177
  if (!isRecord(getOwnEntry(store, "lists"))) {
@@ -184,7 +182,8 @@ function assertValidTaskRecord(taskRecord, list, id, validStates) {
184
182
  if (!isRecord(taskRecord)) {
185
183
  throw malformedTask(list, id, "task");
186
184
  }
187
- if (hasOwnTaskField(taskRecord, "$schema") && getOwnEntry(taskRecord, "$schema") !== TASK_SCHEMA_ID) {
185
+ if (hasOwnTaskField(taskRecord, "$schema") &&
186
+ getOwnEntry(taskRecord, "$schema") !== TASK_SCHEMA_ID) {
188
187
  throw malformedTask(list, id, "$schema");
189
188
  }
190
189
  if (hasOwnTaskField(taskRecord, "kind") && getOwnEntry(taskRecord, "kind") !== TASK_KIND) {
@@ -192,16 +191,12 @@ function assertValidTaskRecord(taskRecord, list, id, validStates) {
192
191
  }
193
192
  if (hasOwnTaskField(taskRecord, "version")) {
194
193
  const version = getOwnEntry(taskRecord, "version");
195
- if (typeof version !== "number" ||
196
- !Number.isInteger(version) ||
197
- version !== TASK_VERSION) {
194
+ if (typeof version !== "number" || !Number.isInteger(version) || version !== TASK_VERSION) {
198
195
  throw malformedTask(list, id, "version");
199
196
  }
200
197
  }
201
198
  const name = getOwnEntry(taskRecord, "name");
202
- if (!hasOwnTaskField(taskRecord, "name") ||
203
- typeof name !== "string" ||
204
- name.length === 0) {
199
+ if (!hasOwnTaskField(taskRecord, "name") || typeof name !== "string" || name.length === 0) {
205
200
  throw malformedTask(list, id, "name");
206
201
  }
207
202
  const state = getOwnEntry(taskRecord, "state");
@@ -396,6 +391,7 @@ function createTasksView(deps, list) {
396
391
  assertCreateDoesNotSetState(input);
397
392
  assertCreateHasId(input);
398
393
  validateTaskId(input.id);
394
+ validateTaskName(input.name);
399
395
  const { document, store } = await readStore(deps.fs, deps.path, validStates);
400
396
  if (getTaskRecord(store, list, input.id)) {
401
397
  throw new TaskAlreadyExistsError(`Task "${list}/${input.id}" already exists.`);
@@ -408,6 +404,9 @@ function createTasksView(deps, list) {
408
404
  async update(id, patch) {
409
405
  assertUpdateDoesNotSetState(patch);
410
406
  validateTaskId(id);
407
+ if (patch.name !== undefined) {
408
+ validateTaskName(patch.name);
409
+ }
411
410
  const { document, store } = await readStore(deps.fs, deps.path, validStates);
412
411
  const existing = getTaskOrThrow(store, list, id);
413
412
  const nextTaskRecord = buildUpdatedTaskRecord(existing, patch);
@@ -500,6 +499,11 @@ function createTasksView(deps, list) {
500
499
  }
501
500
  else {
502
501
  const anchorId = "before" in anchor ? anchor.before : anchor.after;
502
+ const activeIds = new Set(activeItemIds(listNode, validStates));
503
+ if (!activeIds.has(anchorId)) {
504
+ listNode.items.splice(fromIndex, 0, movedPair);
505
+ throw new AnchorNotFoundError(anchorId);
506
+ }
503
507
  const anchorIndex = findItemIndex(listNode, anchorId);
504
508
  if (anchorIndex < 0) {
505
509
  listNode.items.splice(fromIndex, 0, movedPair);
@@ -4,6 +4,9 @@ function isRecord(value) {
4
4
  function isStateList(value) {
5
5
  return Array.isArray(value) && value.every((entry) => typeof entry === "string");
6
6
  }
7
+ function hasVisibleName(value) {
8
+ return value.trim().length > 0;
9
+ }
7
10
  function canFireFromState(event, fromState) {
8
11
  if (event.from === "*") {
9
12
  return event.to !== fromState;
@@ -18,6 +21,9 @@ export function validateMachine(machine) {
18
21
  throw new TypeError("State machine states must be a string array.");
19
22
  }
20
23
  const states = new Set(machine.states);
24
+ if (machine.states.some((state) => !hasVisibleName(state))) {
25
+ throw new Error("State names must not be empty.");
26
+ }
21
27
  if (!hasOwnRecordField(machine, "initial") || typeof machine.initial !== "string") {
22
28
  throw new TypeError("State machine initial must be a string.");
23
29
  }
@@ -28,6 +34,9 @@ export function validateMachine(machine) {
28
34
  throw new TypeError("State machine events must be an object.");
29
35
  }
30
36
  for (const [eventName, event] of Object.entries(machine.events)) {
37
+ if (!hasVisibleName(eventName)) {
38
+ throw new Error("Event names must not be empty.");
39
+ }
31
40
  if (!isRecord(event)) {
32
41
  throw new TypeError(`Event "${eventName}" must be an object.`);
33
42
  }
@@ -20,7 +20,6 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "@poe-code/process-runner": "*",
24
23
  "yaml": "*"
25
24
  }
26
25
  }
@@ -28,13 +28,14 @@ export function createSecretStore(input) {
28
28
  function resolveBackend(input) {
29
29
  const envVar = input.backendEnvVar ?? DEFAULT_BACKEND_ENV_VAR;
30
30
  const configuredBackend = input.backend ?? getOwnEnvValue(input.env, envVar) ?? getOwnEnvValue(process.env, envVar);
31
- if (configuredBackend === "keychain") {
31
+ const backend = configuredBackend?.trim();
32
+ if (backend === "keychain") {
32
33
  return "keychain";
33
34
  }
34
- if (configuredBackend === undefined || configuredBackend === "file") {
35
+ if (backend === undefined || backend === "file") {
35
36
  return "file";
36
37
  }
37
- throw new Error(`Unsupported auth store backend: ${configuredBackend}`);
38
+ throw new Error(`Unsupported auth store backend: ${backend}`);
38
39
  }
39
40
  function getOwnEnvValue(env, key) {
40
41
  return env !== undefined && Object.prototype.hasOwnProperty.call(env, key)
@@ -24,7 +24,10 @@ export class EncryptedFileStore {
24
24
  if (input.filePath === undefined) {
25
25
  const homeDirectory = (input.getHomeDirectory ?? homedir)();
26
26
  const defaultDirectory = input.defaultDirectory ?? ".auth-store";
27
- this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
27
+ const defaultFileName = input.defaultFileName ?? "credentials.enc";
28
+ assertSafeDefaultDirectory(defaultDirectory);
29
+ assertSafeDefaultFileName(defaultFileName);
30
+ this.filePath = path.join(homeDirectory, defaultDirectory, defaultFileName);
28
31
  this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
29
32
  }
30
33
  else {
@@ -154,7 +157,7 @@ function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
154
157
  }
155
158
  function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
156
159
  if (symbolicLinkCheckStartPath === null) {
157
- return [path.dirname(resolvedPath), resolvedPath];
160
+ return getExplicitProtectedCredentialPaths(resolvedPath);
158
161
  }
159
162
  const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
160
163
  if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
@@ -168,6 +171,50 @@ function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
168
171
  }
169
172
  return protectedPaths;
170
173
  }
174
+ function assertSafeDefaultDirectory(defaultDirectory) {
175
+ if (path.isAbsolute(defaultDirectory) || path.win32.isAbsolute(defaultDirectory)) {
176
+ throw new Error("defaultDirectory must be a relative path inside the home directory");
177
+ }
178
+ for (const segment of splitPathSegments(defaultDirectory)) {
179
+ if (segment === "..") {
180
+ throw new Error("defaultDirectory must be a relative path inside the home directory");
181
+ }
182
+ }
183
+ }
184
+ function assertSafeDefaultFileName(defaultFileName) {
185
+ if (defaultFileName.trim().length === 0 ||
186
+ defaultFileName === "." ||
187
+ defaultFileName === ".." ||
188
+ splitPathSegments(defaultFileName).length !== 1) {
189
+ throw new Error("defaultFileName must be a file name without path separators");
190
+ }
191
+ }
192
+ function splitPathSegments(value) {
193
+ return value
194
+ .split("/")
195
+ .flatMap((segment) => segment.split("\\"))
196
+ .filter((segment) => segment.length > 0);
197
+ }
198
+ function getExplicitProtectedCredentialPaths(resolvedPath) {
199
+ const parsed = path.parse(resolvedPath);
200
+ const segments = resolvedPath
201
+ .slice(parsed.root.length)
202
+ .split(path.sep)
203
+ .filter((segment) => segment.length > 0);
204
+ if (segments.length <= 1) {
205
+ return [resolvedPath];
206
+ }
207
+ const protectedPaths = [];
208
+ let currentPath = parsed.root;
209
+ for (const [index, segment] of segments.entries()) {
210
+ currentPath = path.join(currentPath, segment);
211
+ if (index === 0) {
212
+ continue;
213
+ }
214
+ protectedPaths.push(currentPath);
215
+ }
216
+ return protectedPaths;
217
+ }
171
218
  function isPathInsideOrEqual(childPath, parentPath) {
172
219
  const relativePath = path.relative(parentPath, childPath);
173
220
  return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
@@ -7,8 +7,14 @@ export class KeychainStore {
7
7
  account;
8
8
  constructor(input) {
9
9
  this.runCommand = input.runCommand ?? runSecurityCommand;
10
- this.service = input.service;
11
- this.account = input.account;
10
+ this.service = input.service.trim();
11
+ this.account = input.account.trim();
12
+ if (this.service.length === 0) {
13
+ throw new Error("Keychain service must not be empty");
14
+ }
15
+ if (this.account.length === 0) {
16
+ throw new Error("Keychain account must not be empty");
17
+ }
12
18
  }
13
19
  async get() {
14
20
  const result = await this.executeSecurityCommand(["find-generic-password", "-s", this.service, "-a", this.account, "-w"], "read secret from macOS Keychain");
@@ -31,8 +37,9 @@ export class KeychainStore {
31
37
  "-a",
32
38
  this.account,
33
39
  "-U",
34
- "-w"
35
- ], "store secret in macOS Keychain", { stdin: value });
40
+ "-w",
41
+ value
42
+ ], "store secret in macOS Keychain");
36
43
  if (getCommandExitCode(result) !== 0) {
37
44
  throw createSecurityCliFailure("store secret in macOS Keychain", result);
38
45
  }
@@ -14,7 +14,6 @@
14
14
  "scripts": {
15
15
  "build": "node ../../scripts/guard-package-dist.mjs && tsc"
16
16
  },
17
- "dependencies": {},
18
17
  "files": [
19
18
  "dist"
20
19
  ],
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
- import { createSecretStore, } from "auth-store";
3
+ import { createSecretStore } from "auth-store";
4
4
  import { canonicalizeResourceIndicator } from "../resource-indicator.js";
5
5
  const DEFAULT_FILE_SALT = "poe-code:mcp-oauth:v1";
6
6
  const DEFAULT_FILE_DIRECTORY = ".poe-code/mcp-oauth";
@@ -8,6 +8,7 @@ const DEFAULT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth";
8
8
  const DEFAULT_CLIENT_FILE_SALT = "poe-code:mcp-oauth:clients:v1";
9
9
  const DEFAULT_CLIENT_FILE_DIRECTORY = ".poe-code/mcp-oauth/clients";
10
10
  const DEFAULT_CLIENT_KEYCHAIN_SERVICE = "poe-code-mcp-oauth-clients";
11
+ const MAX_JS_DATE_MS = 8_640_000_000_000_000;
11
12
  export function createAuthStoreSessionStore(options = {}) {
12
13
  return {
13
14
  async load(resource) {
@@ -17,10 +18,10 @@ export function createAuthStoreSessionStore(options = {}) {
17
18
  return null;
18
19
  }
19
20
  const parsed = JSON.parse(value);
20
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
21
+ if (isStoredOAuthSession(parsed)) {
21
22
  return parsed;
22
23
  }
23
- throw new Error("Stored OAuth session must be a JSON object");
24
+ throw new Error("Stored OAuth session must match the expected shape");
24
25
  },
25
26
  async save(resource, session) {
26
27
  const store = createResourceSecretStore(resource, options);
@@ -29,7 +30,7 @@ export function createAuthStoreSessionStore(options = {}) {
29
30
  async clear(resource) {
30
31
  const store = createResourceSecretStore(resource, options);
31
32
  await store.delete();
32
- },
33
+ }
33
34
  };
34
35
  }
35
36
  export function createAuthStoreClientStore(options) {
@@ -44,7 +45,8 @@ export function createAuthStoreClientStore(options) {
44
45
  const clientId = isObjectRecord(parsed) ? getOwnString(parsed, "clientId") : undefined;
45
46
  if (clientId !== undefined) {
46
47
  const client = { clientId };
47
- if (isObjectRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
48
+ if (isObjectRecord(parsed) &&
49
+ Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
48
50
  client.clientSecret = getOwnEntry(parsed, "clientSecret");
49
51
  }
50
52
  return client;
@@ -58,7 +60,7 @@ export function createAuthStoreClientStore(options) {
58
60
  async clear(issuer) {
59
61
  const store = createIssuerSecretStore(issuer, options);
60
62
  await store.delete();
61
- },
63
+ }
62
64
  };
63
65
  }
64
66
  function createNamedSecretStore(key, options, defaults) {
@@ -71,16 +73,15 @@ function createNamedSecretStore(key, options, defaults) {
71
73
  ? undefined
72
74
  : path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
73
75
  salt: options.fileStore?.salt ?? defaults.salt,
74
- defaultDirectory: options.fileStore?.defaultDirectory ||
75
- defaults.directory,
76
+ defaultDirectory: options.fileStore?.defaultDirectory || defaults.directory,
76
77
  defaultFileName: parsedFilePath === null
77
78
  ? `${hash}.enc`
78
- : `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`,
79
+ : `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`
79
80
  };
80
81
  const keychainStore = {
81
82
  ...options.keychainStore,
82
83
  service: options.keychainStore?.service ?? defaults.service,
83
- account: `${options.keychainStore?.account ?? defaults.accountPrefix}:${hash}`,
84
+ account: `${options.keychainStore?.account ?? defaults.accountPrefix}:${hash}`
84
85
  };
85
86
  return createSecretStore({ ...options, fileStore, keychainStore }).store;
86
87
  }
@@ -89,7 +90,7 @@ function createResourceSecretStore(resource, options) {
89
90
  salt: DEFAULT_FILE_SALT,
90
91
  directory: DEFAULT_FILE_DIRECTORY,
91
92
  service: DEFAULT_KEYCHAIN_SERVICE,
92
- accountPrefix: "provider",
93
+ accountPrefix: "provider"
93
94
  });
94
95
  }
95
96
  function createIssuerSecretStore(issuer, options) {
@@ -97,7 +98,7 @@ function createIssuerSecretStore(issuer, options) {
97
98
  salt: DEFAULT_CLIENT_FILE_SALT,
98
99
  directory: DEFAULT_CLIENT_FILE_DIRECTORY,
99
100
  service: DEFAULT_CLIENT_KEYCHAIN_SERVICE,
100
- accountPrefix: "issuer",
101
+ accountPrefix: "issuer"
101
102
  });
102
103
  }
103
104
  function isObjectRecord(value) {
@@ -110,3 +111,59 @@ function getOwnString(record, key) {
110
111
  const value = getOwnEntry(record, key);
111
112
  return typeof value === "string" ? value : undefined;
112
113
  }
114
+ function isStoredOAuthSession(value) {
115
+ if (!isObjectRecord(value)) {
116
+ return false;
117
+ }
118
+ return (isNonBlankOwnString(value, "resource") &&
119
+ isNonBlankOwnString(value, "authorizationServer") &&
120
+ isStoredOAuthClient(getOwnEntry(value, "client")) &&
121
+ isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
122
+ isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
123
+ }
124
+ function isStoredOAuthClient(value) {
125
+ if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
126
+ return false;
127
+ }
128
+ const clientSecret = getOwnEntry(value, "clientSecret");
129
+ return (clientSecret === undefined ||
130
+ (typeof clientSecret === "string" && clientSecret.trim().length > 0));
131
+ }
132
+ function isStoredOAuthDiscovery(value) {
133
+ if (!isObjectRecord(value)) {
134
+ return false;
135
+ }
136
+ return (isNonBlankOwnString(value, "resourceMetadataUrl") &&
137
+ isObjectRecord(getOwnEntry(value, "resourceMetadata")) &&
138
+ isObjectRecord(getOwnEntry(value, "authorizationServerMetadata")));
139
+ }
140
+ function isStoredOAuthTokensOrMissing(value) {
141
+ if (value === undefined) {
142
+ return true;
143
+ }
144
+ if (!isObjectRecord(value)) {
145
+ return false;
146
+ }
147
+ if (!isNonBlankOwnString(value, "accessToken") || getOwnString(value, "tokenType") !== "Bearer") {
148
+ return false;
149
+ }
150
+ const expiresAt = getOwnEntry(value, "expiresAt");
151
+ if (expiresAt !== null &&
152
+ (typeof expiresAt !== "number" ||
153
+ !Number.isSafeInteger(expiresAt) ||
154
+ expiresAt > MAX_JS_DATE_MS ||
155
+ !Number.isFinite(new Date(expiresAt).getTime()))) {
156
+ return false;
157
+ }
158
+ const refreshToken = getOwnEntry(value, "refreshToken");
159
+ if (refreshToken !== undefined &&
160
+ (typeof refreshToken !== "string" || refreshToken.trim().length === 0)) {
161
+ return false;
162
+ }
163
+ const scope = getOwnEntry(value, "scope");
164
+ return scope === undefined || (typeof scope === "string" && scope.trim().length > 0);
165
+ }
166
+ function isNonBlankOwnString(record, key) {
167
+ const value = getOwnString(record, key);
168
+ return value !== undefined && value.trim().length > 0;
169
+ }