@byok-sdk/keys 0.1.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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/anthropic-client.d.ts +37 -0
- package/dist/command-runner.d.ts +22 -0
- package/dist/errors.d.ts +78 -0
- package/dist/headers.d.ts +21 -0
- package/dist/http.d.ts +44 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +1462 -0
- package/dist/index.js.map +1 -0
- package/dist/macos-keychain.d.ts +47 -0
- package/dist/openai-client.d.ts +55 -0
- package/dist/profile-store.d.ts +61 -0
- package/dist/provider-profile.d.ts +59 -0
- package/dist/registry.d.ts +103 -0
- package/dist/secret-name.d.ts +30 -0
- package/dist/secret-scope.d.ts +59 -0
- package/dist/secret-store.d.ts +103 -0
- package/dist/sqlite-profile-store.d.ts +35 -0
- package/dist/sqlite-support.d.ts +50 -0
- package/dist/url.d.ts +21 -0
- package/dist/windows-credential-manager.d.ts +31 -0
- package/package.json +47 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AnthropicMessagesClient } from './anthropic-client';
|
|
2
|
+
import type { ProviderFetch } from './http';
|
|
3
|
+
import { OpenAiCompatibleChatClient } from './openai-client';
|
|
4
|
+
import type { ProviderProfileStore } from './profile-store';
|
|
5
|
+
import { type ModelProviderAdapter, type ModelProviderId, type ProviderAuthMode } from './provider-profile';
|
|
6
|
+
import { type ModelProviderSecretName, type SecretStore } from './secret-store';
|
|
7
|
+
/** A transport client for whichever dialect the resolved profile declares. */
|
|
8
|
+
export type ModelProviderClient = AnthropicMessagesClient | OpenAiCompatibleChatClient;
|
|
9
|
+
/**
|
|
10
|
+
* What a caller supplies to {@link ProviderRegistry.configure}: the profile
|
|
11
|
+
* minus the fields the registry owns (`kind`, both timestamps) and minus the
|
|
12
|
+
* secret, which travels as a separate argument so it cannot be mistaken for
|
|
13
|
+
* persisted data.
|
|
14
|
+
*/
|
|
15
|
+
export interface ProviderConfiguration {
|
|
16
|
+
adapter: ModelProviderAdapter;
|
|
17
|
+
auth_mode: ProviderAuthMode;
|
|
18
|
+
base_url: string;
|
|
19
|
+
display_name: string;
|
|
20
|
+
/** Defaults to `true`: configuring a provider makes it the default. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
model: string;
|
|
23
|
+
provider_id: ModelProviderId;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The registry's outward projection of a profile.
|
|
27
|
+
*
|
|
28
|
+
* It reports **whether** a secret exists (`secret_configured`) and never the
|
|
29
|
+
* secret itself — the property `registry.golden.test.ts` asserts, mirroring
|
|
30
|
+
* `docs/researches/HANDOFF-byok-keys.md` §4.3's "status JSON contains no
|
|
31
|
+
* plaintext key".
|
|
32
|
+
*/
|
|
33
|
+
export interface ProviderStatus {
|
|
34
|
+
adapter: ModelProviderAdapter;
|
|
35
|
+
auth_mode: ProviderAuthMode;
|
|
36
|
+
base_url: string;
|
|
37
|
+
created_at: string;
|
|
38
|
+
display_name: string;
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
model: string;
|
|
41
|
+
provider_id: ModelProviderId;
|
|
42
|
+
/** Whether the credential store currently holds this provider's key. */
|
|
43
|
+
secret_configured: boolean;
|
|
44
|
+
updated_at: string;
|
|
45
|
+
}
|
|
46
|
+
export interface ProviderRegistryOptions {
|
|
47
|
+
fetchImpl?: ProviderFetch;
|
|
48
|
+
/** Injected clock, so tests get deterministic timestamps. */
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
profileStore: ProviderProfileStore;
|
|
51
|
+
secretStore: SecretStore<ModelProviderSecretName>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The configure/resolve boundary, ported from `providers.ts:1180-1229`
|
|
55
|
+
* (`configure`) and `providers.ts:1331-1354` (`resolveDefaultModelProvider`).
|
|
56
|
+
*
|
|
57
|
+
* Both halves of a provider's configuration are written here and nowhere else:
|
|
58
|
+
* the non-secret profile goes to the injected {@link ProviderProfileStore}, the
|
|
59
|
+
* API key goes to the injected {@link SecretStore}. Splitting them is the whole
|
|
60
|
+
* point of the package, so the registry is the only place that knows both.
|
|
61
|
+
*
|
|
62
|
+
* Two departures from the source, both required by
|
|
63
|
+
* `docs/researches/HANDOFF-byok-keys.md` §4.5:
|
|
64
|
+
*
|
|
65
|
+
* - `resolveDefaultModelProvider` returns a transport client or `undefined`,
|
|
66
|
+
* and throws on a broken configuration. The source returned an
|
|
67
|
+
* `UnavailableNarrativeProvider` null-object carrying an error code, which is
|
|
68
|
+
* a narrative-domain symbol that stays in aip-main-open — and a degradation
|
|
69
|
+
* fallback this package's fail-closed rule does not permit. A caller that
|
|
70
|
+
* wants aip's behaviour catches `ByokKeysError` and reads `.code`, which is
|
|
71
|
+
* the same information the null-object carried.
|
|
72
|
+
* - The source's `#migrateLegacyModelSecret` is not ported (legacy secret
|
|
73
|
+
* migration is out of scope per the plan).
|
|
74
|
+
*/
|
|
75
|
+
export declare class ProviderRegistry {
|
|
76
|
+
#private;
|
|
77
|
+
constructor(options: ProviderRegistryOptions);
|
|
78
|
+
close(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Persist a provider's profile and, when supplied, its secret
|
|
81
|
+
* (`providers.ts:1180-1229`).
|
|
82
|
+
*
|
|
83
|
+
* Order matters and is the source's: write the secret first, then require
|
|
84
|
+
* that an authenticating profile actually has one, and only then save the
|
|
85
|
+
* profile. A profile is therefore never persisted in a state that claims
|
|
86
|
+
* authentication it cannot perform.
|
|
87
|
+
*/
|
|
88
|
+
configure(configuration: ProviderConfiguration, secret?: string): Promise<ProviderStatus>;
|
|
89
|
+
/** Remove a provider's profile and its secret together. */
|
|
90
|
+
delete(providerId: ModelProviderId): Promise<boolean>;
|
|
91
|
+
get(providerId: ModelProviderId): Promise<ProviderStatus | undefined>;
|
|
92
|
+
list(): Promise<ProviderStatus[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Build a client for the one enabled provider (`providers.ts:1331-1354`).
|
|
95
|
+
*
|
|
96
|
+
* `undefined` means "nothing is configured", which is a legitimate state a
|
|
97
|
+
* caller must handle. A configured-but-broken provider throws instead — a
|
|
98
|
+
* missing secret or an unusable profile is a fault, not an absence.
|
|
99
|
+
*/
|
|
100
|
+
resolveDefaultModelProvider(): Promise<ModelProviderClient | undefined>;
|
|
101
|
+
/** Switch which configured provider is the default. */
|
|
102
|
+
setDefaultModelProvider(providerId: ModelProviderId): Promise<ProviderStatus>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legal secret-entry names, 3 to 96 characters.
|
|
3
|
+
*
|
|
4
|
+
* The source (`aip-main-open@c6a5385`, `apps/local-agent/src/index.ts:258-268`)
|
|
5
|
+
* closed this set at compile time with the `KeychainSecretName` union, so it
|
|
6
|
+
* needed no runtime check. `SecretStore<TName extends string>` is open by
|
|
7
|
+
* design — aip's closed union stays in aip — so the closure moves to runtime.
|
|
8
|
+
*
|
|
9
|
+
* The exclusion that matters is `.`: every backend composes its storage key as
|
|
10
|
+
* `` `${servicePrefix}.${name}` `` and a scoped store appends
|
|
11
|
+
* `.scope.<namespace>` to that prefix. A name containing a dot could therefore
|
|
12
|
+
* spell out another scope's service string and read that scope's secret. The
|
|
13
|
+
* validator throws rather than sanitizing — a caller that mistyped a name must
|
|
14
|
+
* see the error, not silently address a different entry.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SECRET_NAME_PATTERN: RegExp;
|
|
17
|
+
/**
|
|
18
|
+
* Legal scope namespaces, 8 to 96 characters. Ported verbatim from the source's
|
|
19
|
+
* `normalizeSecretNamespace` (`index.ts:748-757`); the 8-character floor keeps
|
|
20
|
+
* a namespace from colliding with a short accidental value.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SECRET_NAMESPACE_PATTERN: RegExp;
|
|
23
|
+
/** Fail closed unless `name` matches {@link SECRET_NAME_PATTERN}. */
|
|
24
|
+
export declare function assertSecretName<TName extends string>(name: TName): TName;
|
|
25
|
+
/**
|
|
26
|
+
* Trim and validate a scope namespace, or throw. Trimming is the source's
|
|
27
|
+
* behavior and is safe because the pattern rejects every interior whitespace
|
|
28
|
+
* character anyway.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertSecretNamespace(value: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type SecretStore } from './secret-store';
|
|
2
|
+
/**
|
|
3
|
+
* Tenant identity a secret store is partitioned by. Ported from
|
|
4
|
+
* `aip-main-open@c6a5385` `apps/local-agent/src/local-data-scope.ts:19-22`
|
|
5
|
+
* (`LocalAccountDataScope`).
|
|
6
|
+
*/
|
|
7
|
+
export interface SecretScope {
|
|
8
|
+
account_id: string;
|
|
9
|
+
workspace_id: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Envelope marker, injectable so K4 can pass the source's
|
|
13
|
+
* `aiphabee-scoped-secrets-v1:` value (`local-data-scope.ts:30`).
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_SECRET_ENVELOPE_PREFIX = "byok-scoped-secrets-v1:";
|
|
16
|
+
/**
|
|
17
|
+
* Derive the opaque namespace a scope's secrets live under
|
|
18
|
+
* (`local-data-scope.ts:32-37`). Hashing means the namespace never leaks a
|
|
19
|
+
* tenant identifier into a keychain service name a user can browse.
|
|
20
|
+
*/
|
|
21
|
+
export declare function secretScopeId(scope: SecretScope): string;
|
|
22
|
+
/**
|
|
23
|
+
* Partition `store` by `scope` (`local-data-scope.ts:100-106`).
|
|
24
|
+
*
|
|
25
|
+
* The source read `store.scope?.(id) ?? new EnvelopeScopedSecretStore(...)`:
|
|
26
|
+
* when a store happened not to implement `scope`, it silently switched to a
|
|
27
|
+
* different storage layout. `SecretStore.scope` is required in this package, so
|
|
28
|
+
* that implicit substitution is gone — a caller who wants envelope semantics
|
|
29
|
+
* constructs {@link EnvelopeScopedSecretStore} on purpose.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scopeSecretStore<TName extends string>(store: SecretStore<TName>, scope: SecretScope): SecretStore<TName>;
|
|
32
|
+
/**
|
|
33
|
+
* Partition a store by keeping every scope's secret in one underlying entry,
|
|
34
|
+
* as a JSON map of scope id to secret.
|
|
35
|
+
*
|
|
36
|
+
* This is the layout to reach for when the backend cannot cheaply namespace its
|
|
37
|
+
* own key space. It costs a read-modify-write on every mutation and offers no
|
|
38
|
+
* cross-process locking, so prefer a backend's native `scope()` when it has
|
|
39
|
+
* one; this decorator exists for the backends that do not.
|
|
40
|
+
*
|
|
41
|
+
* Reads are fail-closed in a way the source's were not
|
|
42
|
+
* (`local-data-scope.ts:163-194`): the source mapped any unparseable stored
|
|
43
|
+
* value to `undefined`, which made a foreign value look like an absent secret
|
|
44
|
+
* and let the next `set()` overwrite it. Here a stored value that is not a
|
|
45
|
+
* well-formed envelope raises `SECRET_ENVELOPE_INVALID`.
|
|
46
|
+
*/
|
|
47
|
+
export declare class EnvelopeScopedSecretStore<TName extends string = string> implements SecretStore<TName> {
|
|
48
|
+
#private;
|
|
49
|
+
readonly providerLabel: string;
|
|
50
|
+
constructor(store: SecretStore<TName>, scopeId: string, options?: {
|
|
51
|
+
envelopePrefix?: string;
|
|
52
|
+
});
|
|
53
|
+
available(): Promise<boolean>;
|
|
54
|
+
delete(name: TName): Promise<boolean>;
|
|
55
|
+
get(name: TName): Promise<string | undefined>;
|
|
56
|
+
has(name: TName): Promise<boolean>;
|
|
57
|
+
scope(namespace: string): SecretStore<TName>;
|
|
58
|
+
set(name: TName, secret: string): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ModelProviderId } from './provider-profile';
|
|
2
|
+
/**
|
|
3
|
+
* Storage contract for a single secret entry in an operating-system credential
|
|
4
|
+
* store.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `aip-main-open@c6a5385` `apps/local-agent/src/index.ts:261-269`
|
|
7
|
+
* with two deliberate changes:
|
|
8
|
+
*
|
|
9
|
+
* 1. `TName` is a generic parameter rather than aip's closed
|
|
10
|
+
* `KeychainSecretName` union. aip's union names device keys, refresh tokens,
|
|
11
|
+
* and market-data entries that this package has no business knowing about,
|
|
12
|
+
* so it stays in aip and consumers pin their own union. The compile-time
|
|
13
|
+
* closure it provided is replaced at runtime by {@link assertSecretName},
|
|
14
|
+
* which every implementation must apply on every name it receives.
|
|
15
|
+
* 2. `scope()` is required, not optional. The source made it optional and its
|
|
16
|
+
* `scopeLocalAgentSecretStore` silently substituted an envelope
|
|
17
|
+
* implementation when a store did not provide one. Scope-envelope prefixing
|
|
18
|
+
* has no installed base, so this package removes the implicit substitution:
|
|
19
|
+
* every store scopes itself, and {@link EnvelopeScopedSecretStore} is a
|
|
20
|
+
* decorator a caller applies on purpose.
|
|
21
|
+
*/
|
|
22
|
+
export interface SecretStore<TName extends string = string> {
|
|
23
|
+
/** Human-readable backend name, safe to show a user. Never contains a secret. */
|
|
24
|
+
readonly providerLabel: string;
|
|
25
|
+
/** Whether this backend can be used on the current machine. Must not throw. */
|
|
26
|
+
available(): Promise<boolean>;
|
|
27
|
+
/** Remove `name`; `false` when it was already absent. */
|
|
28
|
+
delete(name: TName): Promise<boolean>;
|
|
29
|
+
/** Read `name`, or `undefined` when it is absent. */
|
|
30
|
+
get(name: TName): Promise<string | undefined>;
|
|
31
|
+
/** Whether `name` currently holds a secret. */
|
|
32
|
+
has(name: TName): Promise<boolean>;
|
|
33
|
+
/** A view of this store isolated under `namespace`. */
|
|
34
|
+
scope(namespace: string): SecretStore<TName>;
|
|
35
|
+
/** Write `secret` at `name`, replacing any existing value. */
|
|
36
|
+
set(name: TName, secret: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default service prefix for this package's own entries. `servicePrefix` is a
|
|
40
|
+
* constructor option on both OS backends, so K4's aip-main-open swap passes its
|
|
41
|
+
* `com.aiphabee.local-agent` value and keeps existing installs byte-compatible.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_SECRET_SERVICE_PREFIX = "com.byok.keys";
|
|
44
|
+
/**
|
|
45
|
+
* Credential-store entry name per model provider, ported verbatim from
|
|
46
|
+
* `providers.ts:1624-1632`. The names carry no vendor branding — the branding
|
|
47
|
+
* lives in the service prefix — so they travel unchanged and K4 needs no
|
|
48
|
+
* migration.
|
|
49
|
+
*/
|
|
50
|
+
export declare const MODEL_PROVIDER_SECRET_NAMES: {
|
|
51
|
+
readonly anthropic: 'model-anthropic-api-key';
|
|
52
|
+
readonly custom: 'model-custom-api-key';
|
|
53
|
+
readonly deepseek: 'model-deepseek-api-key';
|
|
54
|
+
readonly openai: 'model-openai-api-key';
|
|
55
|
+
};
|
|
56
|
+
export type ModelProviderSecretName = (typeof MODEL_PROVIDER_SECRET_NAMES)[keyof typeof MODEL_PROVIDER_SECRET_NAMES];
|
|
57
|
+
/** Resolve a provider id to the credential-store entry holding its API key. */
|
|
58
|
+
export declare function modelProviderSecretName(providerId: ModelProviderId): ModelProviderSecretName;
|
|
59
|
+
/**
|
|
60
|
+
* The secret-shape invariant both OS backends share. Their size ceilings differ
|
|
61
|
+
* in both magnitude and unit (macOS counts 16384 characters, Windows counts
|
|
62
|
+
* 2560 UTF-8 bytes) and each reports its own error code, so those checks stay
|
|
63
|
+
* in the backends; only the empty/control-character rule is common.
|
|
64
|
+
*/
|
|
65
|
+
export declare function assertSharedSecretValue(secret: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Decode base64 to UTF-8 text, or return `undefined` — never a repaired
|
|
68
|
+
* approximation.
|
|
69
|
+
*
|
|
70
|
+
* Both decode paths in Node are lenient in ways that matter here: `Buffer.from`
|
|
71
|
+
* silently drops characters outside the base64 alphabet (so `"a!G@k="` would
|
|
72
|
+
* decode to `"hi"`), and `Buffer#toString('utf8')` substitutes U+FFFD for
|
|
73
|
+
* invalid byte sequences. A credential store that "successfully" returns a
|
|
74
|
+
* silently-mangled secret is worse than one that fails, so the alphabet, the
|
|
75
|
+
* canonical padding, and the UTF-8 round trip are all checked explicitly.
|
|
76
|
+
*/
|
|
77
|
+
export declare function decodeStrictBase64Utf8(encoded: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* A {@link SecretStore} held in process memory, for tests and for embedders
|
|
80
|
+
* that supply their own persistence.
|
|
81
|
+
*
|
|
82
|
+
* It keys entries by the same `` `${servicePrefix}.${name}` `` string the OS
|
|
83
|
+
* backends use, and `scope()` extends that prefix exactly as they do, so a test
|
|
84
|
+
* written against this store exercises the same isolation arithmetic as
|
|
85
|
+
* production. It applies the name validator and the shared secret-shape rule;
|
|
86
|
+
* the platform size ceilings deliberately are not simulated, since they differ
|
|
87
|
+
* per backend.
|
|
88
|
+
*/
|
|
89
|
+
export declare class InMemorySecretStore<TName extends string = string> implements SecretStore<TName> {
|
|
90
|
+
#private;
|
|
91
|
+
readonly providerLabel = "in-memory";
|
|
92
|
+
constructor(options?: {
|
|
93
|
+
/** Shared backing map; a scoped view keeps the parent's map. */
|
|
94
|
+
entries?: Map<string, string>;
|
|
95
|
+
servicePrefix?: string;
|
|
96
|
+
});
|
|
97
|
+
available(): Promise<boolean>;
|
|
98
|
+
delete(name: TName): Promise<boolean>;
|
|
99
|
+
get(name: TName): Promise<string | undefined>;
|
|
100
|
+
has(name: TName): Promise<boolean>;
|
|
101
|
+
scope(namespace: string): SecretStore<TName>;
|
|
102
|
+
set(name: TName, secret: string): Promise<void>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ProviderProfileStore } from './profile-store';
|
|
2
|
+
import { type ModelProviderId, type ModelProviderProfile } from './provider-profile';
|
|
3
|
+
export interface SqliteProviderProfileStoreOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Database file path. `:memory:` exercises the SQLite code path without a
|
|
6
|
+
* temp file, but defeats the point of this store (restart-safety) exactly as
|
|
7
|
+
* it does for `@byok/server`'s `SqliteTaskStore`.
|
|
8
|
+
*/
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* SQLite-backed {@link ProviderProfileStore}, following `@byok/server`'s
|
|
13
|
+
* `SqliteTaskStore` shape. Holds no secret: the API key lives in the injected
|
|
14
|
+
* `SecretStore`, and `registry.golden.test.ts` asserts the plaintext key never
|
|
15
|
+
* appears in this file's bytes.
|
|
16
|
+
*/
|
|
17
|
+
export declare class SqliteProviderProfileStore implements ProviderProfileStore {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(options: SqliteProviderProfileStoreOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Idempotent, as {@link ProviderProfileStore.close} requires: `node:sqlite`
|
|
22
|
+
* throws "database is not open" on a second `close()`, and a store is
|
|
23
|
+
* routinely closed both by the code that finished with it and by a test's
|
|
24
|
+
* teardown.
|
|
25
|
+
*/
|
|
26
|
+
close(): void;
|
|
27
|
+
delete(providerId: ModelProviderId): boolean;
|
|
28
|
+
get(providerId: ModelProviderId): ModelProviderProfile | undefined;
|
|
29
|
+
getEnabled(): ModelProviderProfile | undefined;
|
|
30
|
+
list(): ModelProviderProfile[];
|
|
31
|
+
save(profile: ModelProviderProfile): ModelProviderProfile;
|
|
32
|
+
setEnabled(providerId: ModelProviderId): ModelProviderProfile;
|
|
33
|
+
}
|
|
34
|
+
/** Exported for the store's own tests to enumerate the CHECK-constrained ids. */
|
|
35
|
+
export declare const SQLITE_PROFILE_PROVIDER_IDS: readonly ["openai", "deepseek", "anthropic", "custom"];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DatabaseSync, DatabaseSyncOptions } from 'node:sqlite';
|
|
2
|
+
interface SqliteModule {
|
|
3
|
+
DatabaseSync: new (path: string, options?: DatabaseSyncOptions) => DatabaseSync;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Load `node:sqlite`, or fail with a `ByokKeysError` that says why.
|
|
7
|
+
*
|
|
8
|
+
* `node:sqlite` shipped in Node.js 22.5.0 and stays marked experimental (an
|
|
9
|
+
* `ExperimentalWarning` on stderr is expected and harmless). Following
|
|
10
|
+
* `@byok/server`'s `sqlite-support.ts`, the SQLite-backed store here depends on
|
|
11
|
+
* nothing else — no `better-sqlite3`, no native module — because zero native
|
|
12
|
+
* dependencies is what keeps this package trivially packageable. The tradeoff
|
|
13
|
+
* is that {@link SqliteProviderProfileStore} does not work below Node 22.5, and
|
|
14
|
+
* this error says so instead of letting a cryptic `Cannot find module` surface
|
|
15
|
+
* from deep inside a query.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadSqliteModule(): SqliteModule;
|
|
18
|
+
/**
|
|
19
|
+
* Whether `node:sqlite` can ACTUALLY be loaded right now.
|
|
20
|
+
*
|
|
21
|
+
* Same predicate as `@byok/server`'s `sqlite-support.ts`, and it exists for the
|
|
22
|
+
* same reason: this package's `engines.node` is `>=20` and CI runs the matrix
|
|
23
|
+
* on 20 and 22, but `node:sqlite` shipped in 22.5 and stayed behind
|
|
24
|
+
* `--experimental-sqlite` for part of the 22.x line. A version-number
|
|
25
|
+
* comparison would therefore be wrong in both directions, so this attempts the
|
|
26
|
+
* real require via {@link loadSqliteModule} and reports whether it succeeded.
|
|
27
|
+
*
|
|
28
|
+
* Callers use it to skip a SQLite-backed path rather than fail it — the
|
|
29
|
+
* package's own SQLite-backed suites gate on it — and anything else should call
|
|
30
|
+
* it before assuming a {@link SqliteProviderProfileStore} can be constructed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isSqliteAvailable(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Open a database, creating its parent directory owner-only first. `:memory:`
|
|
35
|
+
* skips every filesystem step, which is how the shared contract suite exercises
|
|
36
|
+
* the SQLite code path without leaving anything on disk.
|
|
37
|
+
*/
|
|
38
|
+
export declare function openSqliteDatabase(path: string, options?: DatabaseSyncOptions): DatabaseSync;
|
|
39
|
+
/**
|
|
40
|
+
* Restrict `databasePath` and its WAL/SHM siblings to owner-only read/write.
|
|
41
|
+
*
|
|
42
|
+
* The profile table holds no secret — that is the whole point of splitting the
|
|
43
|
+
* key into the OS credential store — but it does hold every provider endpoint
|
|
44
|
+
* this machine talks to, and the source locked the file down
|
|
45
|
+
* (`providers.ts:158`), so the port keeps that. Call after the schema exists,
|
|
46
|
+
* so the lazily-created WAL/SHM files are already there; a sibling that does
|
|
47
|
+
* not exist is skipped rather than treated as an error. No-op for `:memory:`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function secureSqliteFilePermissions(databasePath: string): void;
|
|
50
|
+
export {};
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a provider base URL, fail-closed.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `aip-main-open@c6a5385` `providers.ts:1558-1588` plus the two
|
|
5
|
+
* host predicates at `:2216-2242`. Rules, unchanged:
|
|
6
|
+
* - must be an absolute URL;
|
|
7
|
+
* - no embedded credentials, no fragment, no query string;
|
|
8
|
+
* - HTTPS only, except that HTTP is allowed for loopback hosts;
|
|
9
|
+
* - private-network literals are rejected unless they are loopback;
|
|
10
|
+
* - one trailing slash is stripped.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeProviderUrl(value: string): string;
|
|
13
|
+
/** Whether an already-parseable provider URL points at a loopback host. */
|
|
14
|
+
export declare function isLoopbackProviderUrl(value: string): boolean;
|
|
15
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Conservative private-network literal check. Any IPv6 literal counts as
|
|
18
|
+
* private (the `:` branch), matching the source: the guard cannot cheaply
|
|
19
|
+
* classify IPv6 ranges, so it refuses all of them and lets hostnames through.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPrivateNetworkLiteral(hostname: string): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type CommandRunner } from './command-runner';
|
|
2
|
+
import { type SecretStore } from './secret-store';
|
|
3
|
+
export interface WindowsCredentialManagerSecretStoreOptions {
|
|
4
|
+
account?: string;
|
|
5
|
+
commandRunner?: CommandRunner;
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
servicePrefix?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Windows Credential Manager backend (`index.ts:568-712`).
|
|
11
|
+
*
|
|
12
|
+
* As on macOS there is no plaintext fallback — off win32 every operation throws
|
|
13
|
+
* `CREDENTIAL_MANAGER_UNAVAILABLE`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class WindowsCredentialManagerSecretStore<TName extends string = string> implements SecretStore<TName> {
|
|
16
|
+
#private;
|
|
17
|
+
readonly providerLabel = "Windows Credential Manager";
|
|
18
|
+
constructor(options?: WindowsCredentialManagerSecretStoreOptions);
|
|
19
|
+
available(): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Delete, then read back. The source verifies rather than trusting the
|
|
22
|
+
* delete's exit code (`index.ts:651-679`): a credential that survives a
|
|
23
|
+
* "successful" delete is a security failure, so it is reported as one instead
|
|
24
|
+
* of being returned as `true`.
|
|
25
|
+
*/
|
|
26
|
+
delete(name: TName): Promise<boolean>;
|
|
27
|
+
get(name: TName): Promise<string | undefined>;
|
|
28
|
+
has(name: TName): Promise<boolean>;
|
|
29
|
+
scope(namespace: string): SecretStore<TName>;
|
|
30
|
+
set(name: TName, secret: string): Promise<void>;
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byok-sdk/keys",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BYOK SDK key management: provider profiles, credential-backed auth headers, and direct provider transports",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Ancienttwo/byok-sdk.git",
|
|
10
|
+
"directory": "packages/keys"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ancienttwo/byok-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Ancienttwo/byok-sdk#readme",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"zod": "^4.4.3"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"clean": "rm -rf dist"
|
|
46
|
+
}
|
|
47
|
+
}
|