@forgezero/providers 0.1.22 → 0.1.23
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 +53 -0
- package/dist/binance.js +1 -1
- package/dist/chain.js +1 -1
- package/dist/database.js +1 -1
- package/dist/email.js +1 -1
- package/dist/git.d.ts +124 -0
- package/dist/git.js +400 -0
- package/dist/http.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/pool.js +1 -1
- package/dist/storage.js +1 -1
- package/dist/translation.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ These are supported consumer entry points, not every internal module shipped for
|
|
|
45
45
|
| @forgezero/providers/storage | S3-compatible object storage, SigV4 signed with Web Crypto and no vendor SDK. | portable | [Reference + usage](#forgezero-providers-storage) |
|
|
46
46
|
| @forgezero/providers/translation | Google AI Studio translation with strict batch alignment and classified quota failures. | portable | [Reference + usage](#forgezero-providers-translation) |
|
|
47
47
|
| @forgezero/providers/realtime | Cloudflare KV node-directory and Durable Object fan-out clients for project-defined realtime services. | portable | [Reference + usage](#forgezero-providers-realtime) |
|
|
48
|
+
| @forgezero/providers/git | Versioned Git Connect contracts for accounts, repositories, branches, webhooks and short-lived clone credentials; GitHub, GitLab and private forges attach without changing the service. | portable | [Reference + usage](#forgezero-providers-git) |
|
|
48
49
|
|
|
49
50
|
## Commands
|
|
50
51
|
|
|
@@ -373,11 +374,40 @@ import {
|
|
|
373
374
|
export const selectedCapability = RealtimeProviderError;
|
|
374
375
|
```
|
|
375
376
|
|
|
377
|
+
<a id="forgezero-providers-git"></a>
|
|
378
|
+
## @forgezero/providers/git
|
|
379
|
+
|
|
380
|
+
Versioned Git Connect contracts for accounts, repositories, branches, webhooks and short-lived clone credentials; GitHub, GitLab and private forges attach without changing the service. This is a supported entry point. Import only the named values used by the adjacent task example; the declaration file remains the complete API reference.
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
import {
|
|
384
|
+
defineGitConnectProvider,
|
|
385
|
+
} from '@forgezero/providers/git';
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## @forgezero/providers/git — Use this entry point
|
|
389
|
+
|
|
390
|
+
This minimal executable use imports one concrete value from this exact entry point without a wildcard or package-root detour. Keep the value or values the application actually needs; the full named inventory remains directly above it.
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
import {
|
|
394
|
+
defineGitConnectProvider,
|
|
395
|
+
} from '@forgezero/providers/git';
|
|
396
|
+
|
|
397
|
+
export const selectedCapability = defineGitConnectProvider;
|
|
398
|
+
```
|
|
399
|
+
|
|
376
400
|
## Built-in provider capabilities
|
|
377
401
|
|
|
378
402
|
A provider owns versioned capabilities; it does not own a service. A service method attaches an exact provider, method and version in an ordered array. Current, legacy and deprecated branches can coexist; retired branches never run.
|
|
379
403
|
|
|
380
404
|
```text
|
|
405
|
+
github-app
|
|
406
|
+
- listAccounts
|
|
407
|
+
- listRepositories
|
|
408
|
+
- listBranches
|
|
409
|
+
- ensureWebhook
|
|
410
|
+
- cloneCredential
|
|
381
411
|
evm-rpc
|
|
382
412
|
- request
|
|
383
413
|
jetemail
|
|
@@ -395,6 +425,29 @@ google-ai-studio
|
|
|
395
425
|
- translate
|
|
396
426
|
```
|
|
397
427
|
|
|
428
|
+
## GitHub App (github-app)
|
|
429
|
+
|
|
430
|
+
Import: @forgezero/providers/git. Each method below is a provider capability that a service attaches by exact version. User OAuth verifies an email and is discarded before the mandatory passkey. App installation credentials are minted on demand, limited to one selected repository, and expire rather than being stored. GitLab or a private forge implements the same Git Connect service without pretending to be GitHub.
|
|
431
|
+
|
|
432
|
+
```text
|
|
433
|
+
importPath: '@forgezero/providers/git'
|
|
434
|
+
providerId: 'github-app'
|
|
435
|
+
methods:
|
|
436
|
+
- listAccounts
|
|
437
|
+
- listRepositories
|
|
438
|
+
- listBranches
|
|
439
|
+
- ensureWebhook
|
|
440
|
+
- cloneCredential
|
|
441
|
+
credentials:
|
|
442
|
+
- clientSecret
|
|
443
|
+
- privateKey
|
|
444
|
+
- webhookSecret
|
|
445
|
+
config:
|
|
446
|
+
- clientId
|
|
447
|
+
- appId
|
|
448
|
+
- slug
|
|
449
|
+
```
|
|
450
|
+
|
|
398
451
|
## EVM JSON-RPC node (evm-rpc)
|
|
399
452
|
|
|
400
453
|
Import: @forgezero/providers/chain. Each method below is a provider capability that a service attaches by exact version. Several endpoints per chain is the normal case, not a luxury: public RPC is rate-limited and paid gateways have outages, and a scanner that stops when one is down stops crediting deposits while the money sits on the chain. "Already known" and "nonce too low" are TERMINAL — the transaction is already in flight, so rebroadcasting through another node is how a sweep gets sent twice.
|
package/dist/binance.js
CHANGED
package/dist/chain.js
CHANGED
package/dist/database.js
CHANGED
package/dist/email.js
CHANGED
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type FailureKind, type InvokeContext, type ProviderDefinition } from './index';
|
|
2
|
+
export interface GitAccount {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
kind: 'user' | 'organization' | 'group' | 'other';
|
|
6
|
+
}
|
|
7
|
+
export interface GitRepository {
|
|
8
|
+
id: string;
|
|
9
|
+
fullName: string;
|
|
10
|
+
private: boolean;
|
|
11
|
+
defaultBranch: string;
|
|
12
|
+
cloneUrl: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GitBranch {
|
|
15
|
+
name: string;
|
|
16
|
+
protected?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface GitCloneCredential {
|
|
19
|
+
username: string;
|
|
20
|
+
token: string;
|
|
21
|
+
expiresAtTs: number;
|
|
22
|
+
}
|
|
23
|
+
export interface GitAccountRequest {
|
|
24
|
+
cursor?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface GitRepositoryRequest {
|
|
27
|
+
accountId: string;
|
|
28
|
+
cursor?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface GitBranchRequest {
|
|
31
|
+
accountId: string;
|
|
32
|
+
repository: string;
|
|
33
|
+
cursor?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface GitWebhookRequest {
|
|
36
|
+
accountId: string;
|
|
37
|
+
repository: string;
|
|
38
|
+
callbackUrl: string;
|
|
39
|
+
secret: string;
|
|
40
|
+
events: readonly ['push'];
|
|
41
|
+
}
|
|
42
|
+
export interface GitCloneCredentialRequest {
|
|
43
|
+
accountId: string;
|
|
44
|
+
repository: string;
|
|
45
|
+
}
|
|
46
|
+
export declare const githubAppConfigSchema: {
|
|
47
|
+
readonly type: "object";
|
|
48
|
+
readonly properties: {
|
|
49
|
+
readonly clientId: {
|
|
50
|
+
readonly type: "string";
|
|
51
|
+
readonly minLength: 10;
|
|
52
|
+
readonly maxLength: 128;
|
|
53
|
+
};
|
|
54
|
+
readonly appId: {
|
|
55
|
+
readonly type: "string";
|
|
56
|
+
readonly pattern: "^[1-9][0-9]{0,19}$";
|
|
57
|
+
};
|
|
58
|
+
readonly slug: {
|
|
59
|
+
readonly type: "string";
|
|
60
|
+
readonly pattern: "^[a-z0-9][a-z0-9-]{0,99}$";
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
readonly required: readonly ["clientId", "appId", "slug"];
|
|
64
|
+
readonly additionalProperties: false;
|
|
65
|
+
};
|
|
66
|
+
export declare const githubAppCredentialSchema: {
|
|
67
|
+
readonly type: "object";
|
|
68
|
+
readonly properties: {
|
|
69
|
+
readonly clientSecret: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly minLength: 20;
|
|
72
|
+
readonly maxLength: 512;
|
|
73
|
+
readonly writeOnly: true;
|
|
74
|
+
};
|
|
75
|
+
readonly privateKey: {
|
|
76
|
+
readonly type: "string";
|
|
77
|
+
readonly minLength: 800;
|
|
78
|
+
readonly maxLength: 32768;
|
|
79
|
+
readonly writeOnly: true;
|
|
80
|
+
};
|
|
81
|
+
readonly webhookSecret: {
|
|
82
|
+
readonly type: "string";
|
|
83
|
+
readonly minLength: 32;
|
|
84
|
+
readonly maxLength: 256;
|
|
85
|
+
readonly writeOnly: true;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
readonly required: readonly ["clientSecret", "privateKey", "webhookSecret"];
|
|
89
|
+
readonly additionalProperties: false;
|
|
90
|
+
};
|
|
91
|
+
export declare const gitConnectService: import("./index").ServiceDefinition<"git-connect", {
|
|
92
|
+
listAccounts: import("./index").ServiceMethodContract<GitAccountRequest, readonly GitAccount[]>;
|
|
93
|
+
listRepositories: import("./index").ServiceMethodContract<GitRepositoryRequest, readonly GitRepository[]>;
|
|
94
|
+
listBranches: import("./index").ServiceMethodContract<GitBranchRequest, readonly GitBranch[]>;
|
|
95
|
+
ensureWebhook: import("./index").ServiceMethodContract<GitWebhookRequest, {
|
|
96
|
+
webhookId: string;
|
|
97
|
+
created: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
cloneCredential: import("./index").ServiceMethodContract<GitCloneCredentialRequest, GitCloneCredential>;
|
|
100
|
+
}>;
|
|
101
|
+
export interface GitConnectAdapter {
|
|
102
|
+
listAccounts(context: InvokeContext, args: GitAccountRequest): Promise<readonly GitAccount[]>;
|
|
103
|
+
listRepositories(context: InvokeContext, args: GitRepositoryRequest): Promise<readonly GitRepository[]>;
|
|
104
|
+
listBranches(context: InvokeContext, args: GitBranchRequest): Promise<readonly GitBranch[]>;
|
|
105
|
+
ensureWebhook(context: InvokeContext, args: GitWebhookRequest): Promise<{
|
|
106
|
+
webhookId: string;
|
|
107
|
+
created: boolean;
|
|
108
|
+
}>;
|
|
109
|
+
cloneCredential(context: InvokeContext, args: GitCloneCredentialRequest): Promise<GitCloneCredential>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Define one Git forge without coupling it to ForgeZero Vault or API storage.
|
|
113
|
+
* A GitHub App, GitLab OAuth application, self-hosted forge, or future provider
|
|
114
|
+
* supplies this adapter; the service keeps the same five typed operations.
|
|
115
|
+
*/
|
|
116
|
+
export declare function defineGitConnectProvider(args: {
|
|
117
|
+
id: string;
|
|
118
|
+
label: string;
|
|
119
|
+
version: string;
|
|
120
|
+
adapter: GitConnectAdapter;
|
|
121
|
+
credentials?: Record<string, unknown>;
|
|
122
|
+
config?: Record<string, unknown>;
|
|
123
|
+
classify?: (error: unknown) => FailureKind;
|
|
124
|
+
}): ProviderDefinition;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
class ProviderError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = "ProviderError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function envCredentials(env) {
|
|
13
|
+
return {
|
|
14
|
+
name: "env",
|
|
15
|
+
async get(reference, field) {
|
|
16
|
+
const key = `${reference}_${field}`.replace(/[.-]/g, "_").toUpperCase();
|
|
17
|
+
const value = env[key];
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
throw new ProviderError("CREDENTIAL_MISSING", `Set ${key} in the environment.`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function chainCredentials(...sources) {
|
|
26
|
+
return {
|
|
27
|
+
name: sources.map((source) => source.name).join("+"),
|
|
28
|
+
async get(reference, field) {
|
|
29
|
+
let last;
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
try {
|
|
32
|
+
return await source.get(reference, field);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
last = error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No source held ${reference}.${field}.`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function scopeCredentials(source, reference) {
|
|
42
|
+
return {
|
|
43
|
+
name: `${source.name}:${reference}`,
|
|
44
|
+
get: (field) => source.get(reference, field)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function chainScopedCredentials(...sources) {
|
|
48
|
+
return {
|
|
49
|
+
name: sources.map((source) => source.name).join("+"),
|
|
50
|
+
async get(field) {
|
|
51
|
+
let last;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
try {
|
|
54
|
+
return await source.get(field);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function staticConfig(config) {
|
|
64
|
+
const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
|
|
65
|
+
if (providers.size !== config.providers.length) {
|
|
66
|
+
throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
|
|
67
|
+
}
|
|
68
|
+
const health = new Map;
|
|
69
|
+
return {
|
|
70
|
+
name: "static",
|
|
71
|
+
async provider(instanceKey) {
|
|
72
|
+
const provider = providers.get(instanceKey);
|
|
73
|
+
return provider ? { ...provider, config: { ...provider.config } } : undefined;
|
|
74
|
+
},
|
|
75
|
+
async list(serviceKey, methodKey) {
|
|
76
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
77
|
+
...attachment,
|
|
78
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
|
|
82
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function defineProviderMethod(method) {
|
|
87
|
+
return method;
|
|
88
|
+
}
|
|
89
|
+
function defineProviderMethodBranches(branches) {
|
|
90
|
+
const entries = Object.entries(branches.versions);
|
|
91
|
+
if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
|
|
92
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
|
|
93
|
+
}
|
|
94
|
+
for (const [version, method] of entries) {
|
|
95
|
+
if (method.version !== version) {
|
|
96
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
|
|
97
|
+
}
|
|
98
|
+
if (version === branches.currentVersion && method.lifecycle !== "current") {
|
|
99
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return branches;
|
|
103
|
+
}
|
|
104
|
+
var methodBranch = (provider, methodName, version) => {
|
|
105
|
+
const entry = provider?.methods[methodName];
|
|
106
|
+
if (!entry)
|
|
107
|
+
return;
|
|
108
|
+
if ("versions" in entry)
|
|
109
|
+
return entry.versions[version ?? entry.currentVersion];
|
|
110
|
+
return version === undefined || version === entry.version ? entry : undefined;
|
|
111
|
+
};
|
|
112
|
+
function defineProvider(spec) {
|
|
113
|
+
for (const [name, entry] of Object.entries(spec.methods)) {
|
|
114
|
+
if ("versions" in entry)
|
|
115
|
+
defineProviderMethodBranches(entry);
|
|
116
|
+
else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
|
|
117
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return spec;
|
|
121
|
+
}
|
|
122
|
+
function defineSingleMethodProvider(spec) {
|
|
123
|
+
const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
|
|
124
|
+
return defineProvider({
|
|
125
|
+
...identity,
|
|
126
|
+
methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
var STRIKES_TO_OFFLINE = 3;
|
|
130
|
+
function serviceMethod() {
|
|
131
|
+
return Object.freeze({});
|
|
132
|
+
}
|
|
133
|
+
function defineService(definition) {
|
|
134
|
+
return definition;
|
|
135
|
+
}
|
|
136
|
+
function nextHealth(current, kind) {
|
|
137
|
+
if (kind === "success")
|
|
138
|
+
return { strikes: 0, status: "ok" };
|
|
139
|
+
if (kind === "backoff")
|
|
140
|
+
return current ?? { strikes: 0, status: "ok" };
|
|
141
|
+
const strikes = (current?.strikes ?? 0) + 1;
|
|
142
|
+
return {
|
|
143
|
+
strikes,
|
|
144
|
+
status: strikes >= STRIKES_TO_OFFLINE ? "offline" : "degraded",
|
|
145
|
+
lastFailureAtTs: Date.now()
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function createRegistry(options) {
|
|
149
|
+
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
150
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
151
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
152
|
+
const attempts = [];
|
|
153
|
+
for (const attachment of configured) {
|
|
154
|
+
const requestedVersion = attachment.providerMethodVersion;
|
|
155
|
+
if (callOptions.signal?.aborted) {
|
|
156
|
+
const cancelled = {
|
|
157
|
+
ok: false,
|
|
158
|
+
fallbackUsed: attempts.length > 0,
|
|
159
|
+
attempts,
|
|
160
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
161
|
+
};
|
|
162
|
+
await options.after?.(cancelled);
|
|
163
|
+
return cancelled;
|
|
164
|
+
}
|
|
165
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
166
|
+
if (!instance) {
|
|
167
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const spec = byId.get(instance.providerId);
|
|
171
|
+
const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
|
|
172
|
+
if (!spec || !method) {
|
|
173
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (method.lifecycle === "retired") {
|
|
177
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!instance.enabled) {
|
|
181
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (attachment.health?.status === "offline") {
|
|
185
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
189
|
+
const startedAt = performance.now();
|
|
190
|
+
try {
|
|
191
|
+
const result = await method.invoke({
|
|
192
|
+
config: instance.config,
|
|
193
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
194
|
+
signal: callOptions.signal
|
|
195
|
+
}, args);
|
|
196
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
197
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
|
|
198
|
+
const sent = {
|
|
199
|
+
ok: true,
|
|
200
|
+
result,
|
|
201
|
+
provider: instance.providerId,
|
|
202
|
+
instance: instance.instanceKey,
|
|
203
|
+
method: attachment.providerMethod,
|
|
204
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
|
|
205
|
+
fallbackUsed: attempts.length > 1,
|
|
206
|
+
attempts
|
|
207
|
+
};
|
|
208
|
+
await options.after?.(sent);
|
|
209
|
+
return sent;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (callOptions.signal?.aborted) {
|
|
212
|
+
const cancelled = {
|
|
213
|
+
ok: false,
|
|
214
|
+
fallbackUsed: attempts.length > 0,
|
|
215
|
+
attempts,
|
|
216
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
217
|
+
};
|
|
218
|
+
await options.after?.(cancelled);
|
|
219
|
+
return cancelled;
|
|
220
|
+
}
|
|
221
|
+
const kind = method.classify(error);
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
224
|
+
attempts.push({
|
|
225
|
+
providerId: instance.providerId,
|
|
226
|
+
instanceKey: instance.instanceKey,
|
|
227
|
+
providerMethod: attachment.providerMethod,
|
|
228
|
+
providerMethodVersion: method.version,
|
|
229
|
+
outcome: "failed",
|
|
230
|
+
kind,
|
|
231
|
+
durationMs: performance.now() - startedAt,
|
|
232
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
233
|
+
error: message
|
|
234
|
+
});
|
|
235
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
|
|
236
|
+
if (kind === "terminal") {
|
|
237
|
+
const refused = {
|
|
238
|
+
ok: false,
|
|
239
|
+
fallbackUsed: attempts.length > 1,
|
|
240
|
+
attempts,
|
|
241
|
+
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
242
|
+
};
|
|
243
|
+
await options.after?.(refused);
|
|
244
|
+
return refused;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const failed = {
|
|
249
|
+
ok: false,
|
|
250
|
+
fallbackUsed: attempts.length > 1,
|
|
251
|
+
attempts,
|
|
252
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
253
|
+
};
|
|
254
|
+
await options.after?.(failed);
|
|
255
|
+
return failed;
|
|
256
|
+
}
|
|
257
|
+
function service(definition) {
|
|
258
|
+
return {
|
|
259
|
+
call(method, args, callOptions) {
|
|
260
|
+
return call(definition.key, method, args, callOptions);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { call, service };
|
|
265
|
+
}
|
|
266
|
+
function createService(definition, methods, options = {}) {
|
|
267
|
+
const providers = new Map;
|
|
268
|
+
const credentials = new Map;
|
|
269
|
+
const instances = [];
|
|
270
|
+
const serviceMethods = {};
|
|
271
|
+
for (const methodKey of Object.keys(definition.methods)) {
|
|
272
|
+
const attachments = methods[methodKey];
|
|
273
|
+
if (!Array.isArray(attachments)) {
|
|
274
|
+
throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
|
|
275
|
+
}
|
|
276
|
+
const priorities = new Set;
|
|
277
|
+
serviceMethods[methodKey] = attachments.map((attachment, index) => {
|
|
278
|
+
if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
|
|
279
|
+
throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
|
|
280
|
+
}
|
|
281
|
+
if (priorities.has(attachment.priority)) {
|
|
282
|
+
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
283
|
+
}
|
|
284
|
+
priorities.add(attachment.priority);
|
|
285
|
+
const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
|
|
286
|
+
if (!branch || branch.lifecycle === "retired") {
|
|
287
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
|
|
288
|
+
}
|
|
289
|
+
const existing = providers.get(attachment.provider.id);
|
|
290
|
+
if (existing && existing !== attachment.provider) {
|
|
291
|
+
throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
|
|
292
|
+
}
|
|
293
|
+
providers.set(attachment.provider.id, attachment.provider);
|
|
294
|
+
const internalKey = `${definition.key}:${methodKey}:${index}`;
|
|
295
|
+
credentials.set(internalKey, attachment.credentials);
|
|
296
|
+
instances.push({
|
|
297
|
+
instanceKey: internalKey,
|
|
298
|
+
providerId: attachment.provider.id,
|
|
299
|
+
enabled: attachment.enabled ?? true,
|
|
300
|
+
config: { ...attachment.config ?? {} },
|
|
301
|
+
secretRef: internalKey
|
|
302
|
+
});
|
|
303
|
+
return {
|
|
304
|
+
instanceKey: internalKey,
|
|
305
|
+
providerMethod: attachment.method,
|
|
306
|
+
providerMethodVersion: branch.version,
|
|
307
|
+
priority: attachment.priority,
|
|
308
|
+
enabled: attachment.enabled ?? true
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const registry = createRegistry({
|
|
313
|
+
providers: [...providers.values()],
|
|
314
|
+
config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
|
|
315
|
+
credentials: {
|
|
316
|
+
name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
|
|
317
|
+
async get(reference, field) {
|
|
318
|
+
const source = credentials.get(reference);
|
|
319
|
+
if (!source)
|
|
320
|
+
throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
|
|
321
|
+
return source.get(field);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
...options
|
|
325
|
+
});
|
|
326
|
+
const dynamic = registry.service(definition);
|
|
327
|
+
return {
|
|
328
|
+
async call(method, args, callOptions) {
|
|
329
|
+
const result = await dynamic.call(method, args, callOptions);
|
|
330
|
+
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
331
|
+
return {
|
|
332
|
+
...rest,
|
|
333
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
|
|
334
|
+
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var VERSION = "0.1.23";
|
|
340
|
+
|
|
341
|
+
// src/git.ts
|
|
342
|
+
var githubAppConfigSchema = {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
clientId: { type: "string", minLength: 10, maxLength: 128 },
|
|
346
|
+
appId: { type: "string", pattern: "^[1-9][0-9]{0,19}$" },
|
|
347
|
+
slug: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,99}$" }
|
|
348
|
+
},
|
|
349
|
+
required: ["clientId", "appId", "slug"],
|
|
350
|
+
additionalProperties: false
|
|
351
|
+
};
|
|
352
|
+
var githubAppCredentialSchema = {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
clientSecret: { type: "string", minLength: 20, maxLength: 512, writeOnly: true },
|
|
356
|
+
privateKey: { type: "string", minLength: 800, maxLength: 32768, writeOnly: true },
|
|
357
|
+
webhookSecret: { type: "string", minLength: 32, maxLength: 256, writeOnly: true }
|
|
358
|
+
},
|
|
359
|
+
required: ["clientSecret", "privateKey", "webhookSecret"],
|
|
360
|
+
additionalProperties: false
|
|
361
|
+
};
|
|
362
|
+
var gitConnectService = defineService({
|
|
363
|
+
key: "git-connect",
|
|
364
|
+
methods: {
|
|
365
|
+
listAccounts: serviceMethod(),
|
|
366
|
+
listRepositories: serviceMethod(),
|
|
367
|
+
listBranches: serviceMethod(),
|
|
368
|
+
ensureWebhook: serviceMethod(),
|
|
369
|
+
cloneCredential: serviceMethod()
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
function defineGitConnectProvider(args) {
|
|
373
|
+
const classify = args.classify ?? (() => "retryable");
|
|
374
|
+
const branch = (invoke) => defineProviderMethodBranches({
|
|
375
|
+
currentVersion: args.version,
|
|
376
|
+
versions: {
|
|
377
|
+
[args.version]: { version: args.version, lifecycle: "current", invoke, classify }
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
return defineProvider({
|
|
381
|
+
id: args.id,
|
|
382
|
+
label: args.label,
|
|
383
|
+
multiInstance: true,
|
|
384
|
+
credentials: args.credentials,
|
|
385
|
+
config: args.config,
|
|
386
|
+
methods: {
|
|
387
|
+
listAccounts: branch(args.adapter.listAccounts),
|
|
388
|
+
listRepositories: branch(args.adapter.listRepositories),
|
|
389
|
+
listBranches: branch(args.adapter.listBranches),
|
|
390
|
+
ensureWebhook: branch(args.adapter.ensureWebhook),
|
|
391
|
+
cloneCredential: branch(args.adapter.cloneCredential)
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
export {
|
|
396
|
+
githubAppCredentialSchema,
|
|
397
|
+
githubAppConfigSchema,
|
|
398
|
+
gitConnectService,
|
|
399
|
+
defineGitConnectProvider
|
|
400
|
+
};
|
package/dist/http.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/pool.js
CHANGED
package/dist/storage.js
CHANGED
package/dist/translation.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/providers",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"./realtime": {
|
|
47
47
|
"types": "./dist/realtime.d.ts",
|
|
48
48
|
"default": "./dist/realtime.js"
|
|
49
|
+
},
|
|
50
|
+
"./git": {
|
|
51
|
+
"types": "./dist/git.d.ts",
|
|
52
|
+
"default": "./dist/git.js"
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
"scripts": {
|