larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { dashboardBuildFingerprint } from "../dashboard/dashboard-lifecycle.mjs";
|
|
4
|
+
export function packageVersion(root) {
|
|
5
|
+
if (typeof LARKIN_BUILD_VERSION !== "undefined" && LARKIN_BUILD_VERSION)
|
|
6
|
+
return LARKIN_BUILD_VERSION;
|
|
7
|
+
try {
|
|
8
|
+
return String(JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || "dev");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "dev";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function buildFingerprint(root) {
|
|
15
|
+
if (typeof LARKIN_BUILD_FINGERPRINT !== "undefined" && LARKIN_BUILD_FINGERPRINT)
|
|
16
|
+
return LARKIN_BUILD_FINGERPRINT;
|
|
17
|
+
return dashboardBuildFingerprint(root);
|
|
18
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.callbackCapability = callbackCapability;
|
|
37
|
+
exports.documentCommentCapability = documentCommentCapability;
|
|
38
|
+
exports.documentCommentReplyCapability = documentCommentReplyCapability;
|
|
39
|
+
exports.documentCommentSubscriptionCapability = documentCommentSubscriptionCapability;
|
|
40
|
+
exports.effectiveDocumentCommentSubscription = effectiveDocumentCommentSubscription;
|
|
41
|
+
exports.readDocumentCommentSubscription = readDocumentCommentSubscription;
|
|
42
|
+
exports.readDocumentCommentCapability = readDocumentCommentCapability;
|
|
43
|
+
exports.readDocumentCommentReplyCapability = readDocumentCommentReplyCapability;
|
|
44
|
+
exports.markDocumentCommentSubscriptionVerified = markDocumentCommentSubscriptionVerified;
|
|
45
|
+
exports.readCallbackCapability = readCallbackCapability;
|
|
46
|
+
exports.markCallbackRequested = markCallbackRequested;
|
|
47
|
+
exports.markSetupCapabilitiesRequested = markSetupCapabilitiesRequested;
|
|
48
|
+
exports.issueCallbackProbe = issueCallbackProbe;
|
|
49
|
+
exports.verifyCallbackProbe = verifyCallbackProbe;
|
|
50
|
+
const node_crypto_1 = require("node:crypto");
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
54
|
+
const NONCE = /^[a-f0-9]{32}$/;
|
|
55
|
+
function credentialFile(configDir, appId) {
|
|
56
|
+
if (!APP_ID.test(appId))
|
|
57
|
+
throw new Error("invalid callback capability App ID");
|
|
58
|
+
return path.join(path.resolve(configDir), "bots", `${appId}.json`);
|
|
59
|
+
}
|
|
60
|
+
function readCredential(file) {
|
|
61
|
+
const stat = fs.lstatSync(file);
|
|
62
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600
|
|
63
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
|
|
64
|
+
throw new Error("callback capability credential must be an owned 0600 regular file");
|
|
65
|
+
}
|
|
66
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
68
|
+
throw new Error("callback capability credential is invalid");
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function writeCredential(file, value) {
|
|
72
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
73
|
+
let fd;
|
|
74
|
+
try {
|
|
75
|
+
fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
76
|
+
fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
77
|
+
fs.fsyncSync(fd);
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
fd = undefined;
|
|
80
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
81
|
+
throw new Error("callback capability credential became a symlink");
|
|
82
|
+
fs.renameSync(temporary, file);
|
|
83
|
+
fs.chmodSync(file, 0o600);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
if (fd !== undefined)
|
|
87
|
+
fs.closeSync(fd);
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(temporary);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code !== "ENOENT")
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function callbackCapability(value) {
|
|
98
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
99
|
+
return null;
|
|
100
|
+
const capabilities = value.capabilities;
|
|
101
|
+
const capability = capabilities?.cardActionCallback;
|
|
102
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
103
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
104
|
+
|| Object.keys(capability).some((key) => !["status", "requestedAt", "probeNonce", "probeIssuedAt", "verifiedAt", "verifiedEventId"].includes(key))
|
|
105
|
+
|| !["requested-unverified", "probe-issued", "verified-effective"].includes(capability.status)
|
|
106
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
107
|
+
return null;
|
|
108
|
+
if (capability.status === "probe-issued" && (!capability.probeNonce || !NONCE.test(capability.probeNonce)))
|
|
109
|
+
return null;
|
|
110
|
+
if (capability.status === "verified-effective" && (!capability.verifiedAt || !Number.isFinite(Date.parse(capability.verifiedAt))))
|
|
111
|
+
return null;
|
|
112
|
+
return capability;
|
|
113
|
+
}
|
|
114
|
+
function documentCommentCapability(value) {
|
|
115
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
116
|
+
return null;
|
|
117
|
+
const capabilities = value.capabilities;
|
|
118
|
+
const capability = capabilities?.documentCommentEvent;
|
|
119
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
120
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
121
|
+
|| Object.keys(capability).some((key) => !["status", "event", "scope", "requestedAt"].includes(key))
|
|
122
|
+
|| capability.status !== "requested-unverified"
|
|
123
|
+
|| capability.event !== "drive.notice.comment_add_v1"
|
|
124
|
+
|| capability.scope !== "drive:drive"
|
|
125
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
126
|
+
return null;
|
|
127
|
+
return capability;
|
|
128
|
+
}
|
|
129
|
+
function documentCommentReplyCapability(value) {
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
131
|
+
return null;
|
|
132
|
+
const capabilities = value.capabilities;
|
|
133
|
+
const capability = capabilities?.documentCommentReply;
|
|
134
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
135
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
136
|
+
|| Object.keys(capability).some((key) => !["status", "scope", "requestedAt"].includes(key))
|
|
137
|
+
|| capability.status !== "requested-unverified"
|
|
138
|
+
|| capability.scope !== "docs:document.comment:create"
|
|
139
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
140
|
+
return null;
|
|
141
|
+
return capability;
|
|
142
|
+
}
|
|
143
|
+
function documentCommentSubscriptionCapability(value) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
145
|
+
return null;
|
|
146
|
+
const capabilities = value.capabilities;
|
|
147
|
+
const capability = capabilities?.documentCommentSubscription;
|
|
148
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
149
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype)
|
|
150
|
+
return null;
|
|
151
|
+
const keys = Object.keys(capability);
|
|
152
|
+
if (capability.mode === "none" && capability.status === "safe-default" && capability.source === "setup-default") {
|
|
153
|
+
return keys.length === 4 && keys.every((key) => ["mode", "status", "source", "updatedAt"].includes(key))
|
|
154
|
+
&& typeof capability.updatedAt === "string" && Number.isFinite(Date.parse(capability.updatedAt)) ? capability : null;
|
|
155
|
+
}
|
|
156
|
+
if (capability.mode === "none" && capability.status === "requested-unverified" && capability.source === "setup-opt-in") {
|
|
157
|
+
return keys.length === 5 && keys.every((key) => ["mode", "status", "source", "dimension", "requestedAt"].includes(key))
|
|
158
|
+
&& capability.dimension === "application"
|
|
159
|
+
&& typeof capability.requestedAt === "string" && Number.isFinite(Date.parse(capability.requestedAt)) ? capability : null;
|
|
160
|
+
}
|
|
161
|
+
if (capability.mode === "subscribed" && capability.status === "platform-verified" && capability.source === "platform-status") {
|
|
162
|
+
return keys.length === 6 && keys.every((key) => ["mode", "status", "source", "dimension", "requestedAt", "verifiedAt"].includes(key))
|
|
163
|
+
&& capability.dimension === "application"
|
|
164
|
+
&& typeof capability.requestedAt === "string" && Number.isFinite(Date.parse(capability.requestedAt))
|
|
165
|
+
&& typeof capability.verifiedAt === "string" && Number.isFinite(Date.parse(capability.verifiedAt)) ? capability : null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function effectiveDocumentCommentSubscription(value) {
|
|
170
|
+
const capability = documentCommentSubscriptionCapability(value);
|
|
171
|
+
if (!capability)
|
|
172
|
+
return { mode: "none", status: "safe-default", source: "legacy-default", dimension: null };
|
|
173
|
+
return {
|
|
174
|
+
mode: capability.mode,
|
|
175
|
+
status: capability.status,
|
|
176
|
+
source: capability.source,
|
|
177
|
+
dimension: "dimension" in capability ? capability.dimension : null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function readDocumentCommentSubscription(configDir, appId) {
|
|
181
|
+
return effectiveDocumentCommentSubscription(readCredential(credentialFile(configDir, appId)));
|
|
182
|
+
}
|
|
183
|
+
function readDocumentCommentCapability(configDir, appId) {
|
|
184
|
+
return documentCommentCapability(readCredential(credentialFile(configDir, appId)));
|
|
185
|
+
}
|
|
186
|
+
function readDocumentCommentReplyCapability(configDir, appId) {
|
|
187
|
+
return documentCommentReplyCapability(readCredential(credentialFile(configDir, appId)));
|
|
188
|
+
}
|
|
189
|
+
function markDocumentCommentSubscriptionVerified(configDir, appId, dimension, now = Date.now()) {
|
|
190
|
+
const file = credentialFile(configDir, appId);
|
|
191
|
+
const credential = readCredential(file);
|
|
192
|
+
const prior = documentCommentSubscriptionCapability(credential);
|
|
193
|
+
if (!prior || prior.status !== "requested-unverified" || prior.dimension !== dimension) {
|
|
194
|
+
throw new Error("document comment subscription was not explicitly requested for this dimension");
|
|
195
|
+
}
|
|
196
|
+
const capability = {
|
|
197
|
+
mode: "subscribed",
|
|
198
|
+
status: "platform-verified",
|
|
199
|
+
source: "platform-status",
|
|
200
|
+
dimension,
|
|
201
|
+
requestedAt: prior.requestedAt,
|
|
202
|
+
verifiedAt: new Date(now).toISOString(),
|
|
203
|
+
};
|
|
204
|
+
credential.capabilities = { ...(credential.capabilities || {}), documentCommentSubscription: capability };
|
|
205
|
+
writeCredential(file, credential);
|
|
206
|
+
return capability;
|
|
207
|
+
}
|
|
208
|
+
function readCallbackCapability(configDir, appId) {
|
|
209
|
+
return callbackCapability(readCredential(credentialFile(configDir, appId)));
|
|
210
|
+
}
|
|
211
|
+
function markCallbackRequested(configDir, appId, now = Date.now()) {
|
|
212
|
+
const file = credentialFile(configDir, appId);
|
|
213
|
+
const credential = readCredential(file);
|
|
214
|
+
const prior = callbackCapability(credential);
|
|
215
|
+
const capability = {
|
|
216
|
+
status: "requested-unverified",
|
|
217
|
+
requestedAt: prior?.requestedAt || new Date(now).toISOString(),
|
|
218
|
+
};
|
|
219
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback: capability };
|
|
220
|
+
writeCredential(file, credential);
|
|
221
|
+
return capability;
|
|
222
|
+
}
|
|
223
|
+
function markSetupCapabilitiesRequested(configDir, appId, now = Date.now(), commentSubscription) {
|
|
224
|
+
const file = credentialFile(configDir, appId);
|
|
225
|
+
const credential = readCredential(file);
|
|
226
|
+
const priorCallback = callbackCapability(credential);
|
|
227
|
+
const priorComment = documentCommentCapability(credential);
|
|
228
|
+
const priorReply = documentCommentReplyCapability(credential);
|
|
229
|
+
const priorSubscription = documentCommentSubscriptionCapability(credential);
|
|
230
|
+
const requestedAt = new Date(now).toISOString();
|
|
231
|
+
const cardActionCallback = {
|
|
232
|
+
status: "requested-unverified",
|
|
233
|
+
requestedAt: priorCallback?.requestedAt || requestedAt,
|
|
234
|
+
};
|
|
235
|
+
const documentCommentEvent = {
|
|
236
|
+
status: "requested-unverified",
|
|
237
|
+
event: "drive.notice.comment_add_v1",
|
|
238
|
+
scope: "drive:drive",
|
|
239
|
+
requestedAt: priorComment?.requestedAt || requestedAt,
|
|
240
|
+
};
|
|
241
|
+
const documentCommentReply = {
|
|
242
|
+
status: "requested-unverified",
|
|
243
|
+
scope: "docs:document.comment:create",
|
|
244
|
+
requestedAt: priorReply?.requestedAt || requestedAt,
|
|
245
|
+
};
|
|
246
|
+
const documentCommentSubscription = commentSubscription === "application"
|
|
247
|
+
? {
|
|
248
|
+
mode: "none",
|
|
249
|
+
status: "requested-unverified",
|
|
250
|
+
source: "setup-opt-in",
|
|
251
|
+
dimension: commentSubscription,
|
|
252
|
+
requestedAt: requestedAt,
|
|
253
|
+
}
|
|
254
|
+
: commentSubscription === "none" || !priorSubscription
|
|
255
|
+
? { mode: "none", status: "safe-default", source: "setup-default", updatedAt: requestedAt }
|
|
256
|
+
: priorSubscription;
|
|
257
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback, documentCommentEvent, documentCommentReply, documentCommentSubscription };
|
|
258
|
+
writeCredential(file, credential);
|
|
259
|
+
return { cardActionCallback, documentCommentEvent, documentCommentReply, documentCommentSubscription };
|
|
260
|
+
}
|
|
261
|
+
function issueCallbackProbe(configDir, appId, now = Date.now(), random = node_crypto_1.randomUUID) {
|
|
262
|
+
const file = credentialFile(configDir, appId);
|
|
263
|
+
const credential = readCredential(file);
|
|
264
|
+
const prior = callbackCapability(credential);
|
|
265
|
+
if (!prior)
|
|
266
|
+
throw new Error("card.action.trigger was not requested by setup; rerun larkin setup first");
|
|
267
|
+
if (prior.status === "verified-effective")
|
|
268
|
+
return { nonce: "", capability: prior };
|
|
269
|
+
const nonce = random().replaceAll("-", "").slice(0, 32);
|
|
270
|
+
if (!NONCE.test(nonce))
|
|
271
|
+
throw new Error("callback probe nonce generator returned an invalid value");
|
|
272
|
+
const capability = {
|
|
273
|
+
status: "probe-issued",
|
|
274
|
+
requestedAt: prior.requestedAt,
|
|
275
|
+
probeNonce: nonce,
|
|
276
|
+
probeIssuedAt: new Date(now).toISOString(),
|
|
277
|
+
};
|
|
278
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback: capability };
|
|
279
|
+
writeCredential(file, credential);
|
|
280
|
+
return { nonce, capability };
|
|
281
|
+
}
|
|
282
|
+
function verifyCallbackProbe(configDir, appId, nonce, eventId, now = Date.now()) {
|
|
283
|
+
if (!NONCE.test(nonce) || !eventId)
|
|
284
|
+
return false;
|
|
285
|
+
const file = credentialFile(configDir, appId);
|
|
286
|
+
const credential = readCredential(file);
|
|
287
|
+
const prior = callbackCapability(credential);
|
|
288
|
+
if (!prior || prior.status !== "probe-issued" || prior.probeNonce !== nonce)
|
|
289
|
+
return false;
|
|
290
|
+
credential.capabilities = {
|
|
291
|
+
...(credential.capabilities || {}),
|
|
292
|
+
cardActionCallback: {
|
|
293
|
+
status: "verified-effective",
|
|
294
|
+
requestedAt: prior.requestedAt,
|
|
295
|
+
verifiedAt: new Date(now).toISOString(),
|
|
296
|
+
verifiedEventId: eventId,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
writeCredential(file, credential);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
5
|
+
const NONCE = /^[a-f0-9]{32}$/;
|
|
6
|
+
function credentialFile(configDir, appId) {
|
|
7
|
+
if (!APP_ID.test(appId))
|
|
8
|
+
throw new Error("invalid callback capability App ID");
|
|
9
|
+
return path.join(path.resolve(configDir), "bots", `${appId}.json`);
|
|
10
|
+
}
|
|
11
|
+
function readCredential(file) {
|
|
12
|
+
const stat = fs.lstatSync(file);
|
|
13
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o600
|
|
14
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
|
|
15
|
+
throw new Error("callback capability credential must be an owned 0600 regular file");
|
|
16
|
+
}
|
|
17
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
19
|
+
throw new Error("callback capability credential is invalid");
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function writeCredential(file, value) {
|
|
23
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
|
|
24
|
+
let fd;
|
|
25
|
+
try {
|
|
26
|
+
fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
27
|
+
fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
28
|
+
fs.fsyncSync(fd);
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
fd = undefined;
|
|
31
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
32
|
+
throw new Error("callback capability credential became a symlink");
|
|
33
|
+
fs.renameSync(temporary, file);
|
|
34
|
+
fs.chmodSync(file, 0o600);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (fd !== undefined)
|
|
38
|
+
fs.closeSync(fd);
|
|
39
|
+
try {
|
|
40
|
+
fs.unlinkSync(temporary);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code !== "ENOENT")
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function callbackCapability(value) {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
50
|
+
return null;
|
|
51
|
+
const capabilities = value.capabilities;
|
|
52
|
+
const capability = capabilities?.cardActionCallback;
|
|
53
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
54
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
55
|
+
|| Object.keys(capability).some((key) => !["status", "requestedAt", "probeNonce", "probeIssuedAt", "verifiedAt", "verifiedEventId"].includes(key))
|
|
56
|
+
|| !["requested-unverified", "probe-issued", "verified-effective"].includes(capability.status)
|
|
57
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
58
|
+
return null;
|
|
59
|
+
if (capability.status === "probe-issued" && (!capability.probeNonce || !NONCE.test(capability.probeNonce)))
|
|
60
|
+
return null;
|
|
61
|
+
if (capability.status === "verified-effective" && (!capability.verifiedAt || !Number.isFinite(Date.parse(capability.verifiedAt))))
|
|
62
|
+
return null;
|
|
63
|
+
return capability;
|
|
64
|
+
}
|
|
65
|
+
export function documentCommentCapability(value) {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
67
|
+
return null;
|
|
68
|
+
const capabilities = value.capabilities;
|
|
69
|
+
const capability = capabilities?.documentCommentEvent;
|
|
70
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
71
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
72
|
+
|| Object.keys(capability).some((key) => !["status", "event", "scope", "requestedAt"].includes(key))
|
|
73
|
+
|| capability.status !== "requested-unverified"
|
|
74
|
+
|| capability.event !== "drive.notice.comment_add_v1"
|
|
75
|
+
|| capability.scope !== "drive:drive"
|
|
76
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
77
|
+
return null;
|
|
78
|
+
return capability;
|
|
79
|
+
}
|
|
80
|
+
export function documentCommentReplyCapability(value) {
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return null;
|
|
83
|
+
const capabilities = value.capabilities;
|
|
84
|
+
const capability = capabilities?.documentCommentReply;
|
|
85
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
86
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype
|
|
87
|
+
|| Object.keys(capability).some((key) => !["status", "scope", "requestedAt"].includes(key))
|
|
88
|
+
|| capability.status !== "requested-unverified"
|
|
89
|
+
|| capability.scope !== "docs:document.comment:create"
|
|
90
|
+
|| typeof capability.requestedAt !== "string" || !Number.isFinite(Date.parse(capability.requestedAt)))
|
|
91
|
+
return null;
|
|
92
|
+
return capability;
|
|
93
|
+
}
|
|
94
|
+
export function documentCommentSubscriptionCapability(value) {
|
|
95
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
96
|
+
return null;
|
|
97
|
+
const capabilities = value.capabilities;
|
|
98
|
+
const capability = capabilities?.documentCommentSubscription;
|
|
99
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities) || Object.getPrototypeOf(capabilities) !== Object.prototype
|
|
100
|
+
|| !capability || typeof capability !== "object" || Array.isArray(capability) || Object.getPrototypeOf(capability) !== Object.prototype)
|
|
101
|
+
return null;
|
|
102
|
+
const keys = Object.keys(capability);
|
|
103
|
+
if (capability.mode === "none" && capability.status === "safe-default" && capability.source === "setup-default") {
|
|
104
|
+
return keys.length === 4 && keys.every((key) => ["mode", "status", "source", "updatedAt"].includes(key))
|
|
105
|
+
&& typeof capability.updatedAt === "string" && Number.isFinite(Date.parse(capability.updatedAt)) ? capability : null;
|
|
106
|
+
}
|
|
107
|
+
if (capability.mode === "none" && capability.status === "requested-unverified" && capability.source === "setup-opt-in") {
|
|
108
|
+
return keys.length === 5 && keys.every((key) => ["mode", "status", "source", "dimension", "requestedAt"].includes(key))
|
|
109
|
+
&& capability.dimension === "application"
|
|
110
|
+
&& typeof capability.requestedAt === "string" && Number.isFinite(Date.parse(capability.requestedAt)) ? capability : null;
|
|
111
|
+
}
|
|
112
|
+
if (capability.mode === "subscribed" && capability.status === "platform-verified" && capability.source === "platform-status") {
|
|
113
|
+
return keys.length === 6 && keys.every((key) => ["mode", "status", "source", "dimension", "requestedAt", "verifiedAt"].includes(key))
|
|
114
|
+
&& capability.dimension === "application"
|
|
115
|
+
&& typeof capability.requestedAt === "string" && Number.isFinite(Date.parse(capability.requestedAt))
|
|
116
|
+
&& typeof capability.verifiedAt === "string" && Number.isFinite(Date.parse(capability.verifiedAt)) ? capability : null;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
export function effectiveDocumentCommentSubscription(value) {
|
|
121
|
+
const capability = documentCommentSubscriptionCapability(value);
|
|
122
|
+
if (!capability)
|
|
123
|
+
return { mode: "none", status: "safe-default", source: "legacy-default", dimension: null };
|
|
124
|
+
return {
|
|
125
|
+
mode: capability.mode,
|
|
126
|
+
status: capability.status,
|
|
127
|
+
source: capability.source,
|
|
128
|
+
dimension: "dimension" in capability ? capability.dimension : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function readDocumentCommentSubscription(configDir, appId) {
|
|
132
|
+
return effectiveDocumentCommentSubscription(readCredential(credentialFile(configDir, appId)));
|
|
133
|
+
}
|
|
134
|
+
export function readDocumentCommentCapability(configDir, appId) {
|
|
135
|
+
return documentCommentCapability(readCredential(credentialFile(configDir, appId)));
|
|
136
|
+
}
|
|
137
|
+
export function readDocumentCommentReplyCapability(configDir, appId) {
|
|
138
|
+
return documentCommentReplyCapability(readCredential(credentialFile(configDir, appId)));
|
|
139
|
+
}
|
|
140
|
+
export function markDocumentCommentSubscriptionVerified(configDir, appId, dimension, now = Date.now()) {
|
|
141
|
+
const file = credentialFile(configDir, appId);
|
|
142
|
+
const credential = readCredential(file);
|
|
143
|
+
const prior = documentCommentSubscriptionCapability(credential);
|
|
144
|
+
if (!prior || prior.status !== "requested-unverified" || prior.dimension !== dimension) {
|
|
145
|
+
throw new Error("document comment subscription was not explicitly requested for this dimension");
|
|
146
|
+
}
|
|
147
|
+
const capability = {
|
|
148
|
+
mode: "subscribed",
|
|
149
|
+
status: "platform-verified",
|
|
150
|
+
source: "platform-status",
|
|
151
|
+
dimension,
|
|
152
|
+
requestedAt: prior.requestedAt,
|
|
153
|
+
verifiedAt: new Date(now).toISOString(),
|
|
154
|
+
};
|
|
155
|
+
credential.capabilities = { ...(credential.capabilities || {}), documentCommentSubscription: capability };
|
|
156
|
+
writeCredential(file, credential);
|
|
157
|
+
return capability;
|
|
158
|
+
}
|
|
159
|
+
export function readCallbackCapability(configDir, appId) {
|
|
160
|
+
return callbackCapability(readCredential(credentialFile(configDir, appId)));
|
|
161
|
+
}
|
|
162
|
+
export function markCallbackRequested(configDir, appId, now = Date.now()) {
|
|
163
|
+
const file = credentialFile(configDir, appId);
|
|
164
|
+
const credential = readCredential(file);
|
|
165
|
+
const prior = callbackCapability(credential);
|
|
166
|
+
const capability = {
|
|
167
|
+
status: "requested-unverified",
|
|
168
|
+
requestedAt: prior?.requestedAt || new Date(now).toISOString(),
|
|
169
|
+
};
|
|
170
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback: capability };
|
|
171
|
+
writeCredential(file, credential);
|
|
172
|
+
return capability;
|
|
173
|
+
}
|
|
174
|
+
export function markSetupCapabilitiesRequested(configDir, appId, now = Date.now(), commentSubscription) {
|
|
175
|
+
const file = credentialFile(configDir, appId);
|
|
176
|
+
const credential = readCredential(file);
|
|
177
|
+
const priorCallback = callbackCapability(credential);
|
|
178
|
+
const priorComment = documentCommentCapability(credential);
|
|
179
|
+
const priorReply = documentCommentReplyCapability(credential);
|
|
180
|
+
const priorSubscription = documentCommentSubscriptionCapability(credential);
|
|
181
|
+
const requestedAt = new Date(now).toISOString();
|
|
182
|
+
const cardActionCallback = {
|
|
183
|
+
status: "requested-unverified",
|
|
184
|
+
requestedAt: priorCallback?.requestedAt || requestedAt,
|
|
185
|
+
};
|
|
186
|
+
const documentCommentEvent = {
|
|
187
|
+
status: "requested-unverified",
|
|
188
|
+
event: "drive.notice.comment_add_v1",
|
|
189
|
+
scope: "drive:drive",
|
|
190
|
+
requestedAt: priorComment?.requestedAt || requestedAt,
|
|
191
|
+
};
|
|
192
|
+
const documentCommentReply = {
|
|
193
|
+
status: "requested-unverified",
|
|
194
|
+
scope: "docs:document.comment:create",
|
|
195
|
+
requestedAt: priorReply?.requestedAt || requestedAt,
|
|
196
|
+
};
|
|
197
|
+
const documentCommentSubscription = commentSubscription === "application"
|
|
198
|
+
? {
|
|
199
|
+
mode: "none",
|
|
200
|
+
status: "requested-unverified",
|
|
201
|
+
source: "setup-opt-in",
|
|
202
|
+
dimension: commentSubscription,
|
|
203
|
+
requestedAt: requestedAt,
|
|
204
|
+
}
|
|
205
|
+
: commentSubscription === "none" || !priorSubscription
|
|
206
|
+
? { mode: "none", status: "safe-default", source: "setup-default", updatedAt: requestedAt }
|
|
207
|
+
: priorSubscription;
|
|
208
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback, documentCommentEvent, documentCommentReply, documentCommentSubscription };
|
|
209
|
+
writeCredential(file, credential);
|
|
210
|
+
return { cardActionCallback, documentCommentEvent, documentCommentReply, documentCommentSubscription };
|
|
211
|
+
}
|
|
212
|
+
export function issueCallbackProbe(configDir, appId, now = Date.now(), random = randomUUID) {
|
|
213
|
+
const file = credentialFile(configDir, appId);
|
|
214
|
+
const credential = readCredential(file);
|
|
215
|
+
const prior = callbackCapability(credential);
|
|
216
|
+
if (!prior)
|
|
217
|
+
throw new Error("card.action.trigger was not requested by setup; rerun larkin setup first");
|
|
218
|
+
if (prior.status === "verified-effective")
|
|
219
|
+
return { nonce: "", capability: prior };
|
|
220
|
+
const nonce = random().replaceAll("-", "").slice(0, 32);
|
|
221
|
+
if (!NONCE.test(nonce))
|
|
222
|
+
throw new Error("callback probe nonce generator returned an invalid value");
|
|
223
|
+
const capability = {
|
|
224
|
+
status: "probe-issued",
|
|
225
|
+
requestedAt: prior.requestedAt,
|
|
226
|
+
probeNonce: nonce,
|
|
227
|
+
probeIssuedAt: new Date(now).toISOString(),
|
|
228
|
+
};
|
|
229
|
+
credential.capabilities = { ...(credential.capabilities || {}), cardActionCallback: capability };
|
|
230
|
+
writeCredential(file, credential);
|
|
231
|
+
return { nonce, capability };
|
|
232
|
+
}
|
|
233
|
+
export function verifyCallbackProbe(configDir, appId, nonce, eventId, now = Date.now()) {
|
|
234
|
+
if (!NONCE.test(nonce) || !eventId)
|
|
235
|
+
return false;
|
|
236
|
+
const file = credentialFile(configDir, appId);
|
|
237
|
+
const credential = readCredential(file);
|
|
238
|
+
const prior = callbackCapability(credential);
|
|
239
|
+
if (!prior || prior.status !== "probe-issued" || prior.probeNonce !== nonce)
|
|
240
|
+
return false;
|
|
241
|
+
credential.capabilities = {
|
|
242
|
+
...(credential.capabilities || {}),
|
|
243
|
+
cardActionCallback: {
|
|
244
|
+
status: "verified-effective",
|
|
245
|
+
requestedAt: prior.requestedAt,
|
|
246
|
+
verifiedAt: new Date(now).toISOString(),
|
|
247
|
+
verifiedEventId: eventId,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
writeCredential(file, credential);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const REQUIRED = "1.3.14";
|
|
3
|
+
function supported(version) {
|
|
4
|
+
return String(version || "") === REQUIRED;
|
|
5
|
+
}
|
|
6
|
+
function assertSupportedBun(version = globalThis.Bun?.version) {
|
|
7
|
+
if (supported(version))
|
|
8
|
+
return;
|
|
9
|
+
throw new Error(`Larkin requires Bun ${REQUIRED} (current: ${version || "unknown"}). Install the pinned toolchain from packageManager before running Larkin.`);
|
|
10
|
+
}
|
|
11
|
+
assertSupportedBun();
|
|
12
|
+
module.exports = { REQUIRED, supported, assertSupportedBun };
|