zitejs 0.10.0-start.12 → 0.10.0-start.14
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/dist/cjs/internal/appMode.d.ts +3 -0
- package/dist/cjs/internal/appMode.js +15 -0
- package/dist/cjs/upload/index.js +12 -2
- package/dist/cjs/upload/index.test.js +39 -0
- package/dist/esm/internal/appMode.d.ts +3 -0
- package/dist/esm/internal/appMode.js +10 -0
- package/dist/esm/start/aiUsage.d.ts +23 -0
- package/dist/esm/start/aiUsage.js +146 -0
- package/dist/esm/start/airtable/index.d.ts +2 -5
- package/dist/esm/start/airtable/index.js +12 -28
- package/dist/esm/start/credentials.d.ts +2 -0
- package/dist/esm/start/credentials.js +2 -0
- package/dist/esm/start/data/index.d.ts +2 -5
- package/dist/esm/start/data/index.js +4 -22
- package/dist/esm/start/outbound.d.ts +11 -0
- package/dist/esm/start/outbound.js +410 -0
- package/dist/esm/start/redactSecrets.d.ts +8 -0
- package/dist/esm/start/redactSecrets.js +23 -0
- package/dist/esm/start/router.d.ts +6 -1
- package/dist/esm/start/router.js +10 -4
- package/dist/esm/start/server.d.ts +2 -2
- package/dist/esm/start/server.js +37 -29
- package/dist/esm/start/types.d.ts +50 -8
- package/dist/esm/start/version.d.ts +1 -1
- package/dist/esm/start/version.js +1 -1
- package/dist/esm/upload/index.js +12 -2
- package/dist/esm/upload/index.test.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.platformMode = exports.keepRenderedMode = void 0;
|
|
4
|
+
// Classic's upload client reads this too, so it imports nothing of Start: the
|
|
5
|
+
// request's host comes through the key src/start/host.ts reads it under.
|
|
6
|
+
const HOST_STORE_KEY = Symbol.for("zitejs:host-context");
|
|
7
|
+
// What a Start page was rendered with, kept in the browser when it hydrates.
|
|
8
|
+
const RENDERED_MODE = Symbol.for("zitejs:rendered-mode");
|
|
9
|
+
const keepRenderedMode = (mode) => {
|
|
10
|
+
globalThis[RENDERED_MODE] = mode;
|
|
11
|
+
};
|
|
12
|
+
exports.keepRenderedMode = keepRenderedMode;
|
|
13
|
+
/** The mode the platform served this request or page in; undefined outside a Start app. */
|
|
14
|
+
const platformMode = () => globalThis[HOST_STORE_KEY]?.getStore()?.mode ?? globalThis[RENDERED_MODE];
|
|
15
|
+
exports.platformMode = platformMode;
|
package/dist/cjs/upload/index.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.uploadFile = uploadFile;
|
|
|
6
6
|
exports.useUpload = useUpload;
|
|
7
7
|
const react_1 = require("react");
|
|
8
8
|
const config_js_1 = require("../auth/config.js");
|
|
9
|
+
const appMode_js_1 = require("../internal/appMode.js");
|
|
9
10
|
const constants_js_1 = require("../auth/constants.js");
|
|
10
11
|
class FileUploadError extends Error {
|
|
11
12
|
constructor(message) {
|
|
@@ -14,7 +15,16 @@ class FileUploadError extends Error {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
exports.FileUploadError = FileUploadError;
|
|
17
|
-
|
|
18
|
+
/** As the platform served the page or request; a classic app's from its hostname. */
|
|
19
|
+
function getZiteAppMode() {
|
|
20
|
+
const served = (0, appMode_js_1.platformMode)();
|
|
21
|
+
if (served)
|
|
22
|
+
return served === 'preview' || served === 'validate' ? 'preview' : 'live';
|
|
23
|
+
// On a server, outside any request the platform served: there's no telling a preview from live.
|
|
24
|
+
if (typeof window === 'undefined') {
|
|
25
|
+
throw new FileUploadError('Upload from a page, or from a request the platform is serving');
|
|
26
|
+
}
|
|
27
|
+
const { hostname } = window.location;
|
|
18
28
|
const maybeId = hostname.split('.')[0];
|
|
19
29
|
if (maybeId?.startsWith(constants_js_1.ZITE_APP_EDITOR_HOSTNAME_PREFIX))
|
|
20
30
|
return 'preview';
|
|
@@ -82,7 +92,7 @@ function authHeaders() {
|
|
|
82
92
|
}
|
|
83
93
|
async function performUpload(data, filename) {
|
|
84
94
|
const blob = toUploadBlob(data);
|
|
85
|
-
const mode = getZiteAppMode(
|
|
95
|
+
const mode = getZiteAppMode();
|
|
86
96
|
const flowId = (0, config_js_1.getFlowId)();
|
|
87
97
|
const sessionRes = await fetch((0, config_js_1.getApiUrl)() + '/v1/zite/public/' + flowId + '/upload-session?mode=' + mode, {
|
|
88
98
|
method: 'POST',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const vitest_1 = require("vitest");
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
const appMode_js_1 = require("../internal/appMode.js");
|
|
4
6
|
const index_js_1 = require("./index.js");
|
|
5
7
|
vitest_1.vi.mock('../auth/config.js', () => ({
|
|
6
8
|
getApiUrl: () => 'https://api.example.com',
|
|
@@ -93,3 +95,40 @@ const fetchMock = vitest_1.vi.fn();
|
|
|
93
95
|
await (0, vitest_1.expect)(upload).rejects.toThrow('File size exceeds maximum of 20 MB');
|
|
94
96
|
});
|
|
95
97
|
});
|
|
98
|
+
// SDK-10: a Start app's mode is the platform's, never guessed from where the page is served.
|
|
99
|
+
(0, vitest_1.describe)('the mode an upload goes under', () => {
|
|
100
|
+
const sessionUrl = async () => {
|
|
101
|
+
fetchMock.mockResolvedValueOnce({
|
|
102
|
+
ok: true,
|
|
103
|
+
json: async () => ({ success: true, presignedUrl: 'https://s3.example.com/put', fileUrl: 'https://uploads.zite.com/f.txt' }),
|
|
104
|
+
});
|
|
105
|
+
fetchMock.mockResolvedValueOnce({ ok: true });
|
|
106
|
+
await (0, index_js_1.uploadFile)({ data: 'hi', filename: 'f.txt' });
|
|
107
|
+
return String(fetchMock.mock.calls[0][0]);
|
|
108
|
+
};
|
|
109
|
+
(0, vitest_1.afterEach)(() => (0, appMode_js_1.keepRenderedMode)(undefined));
|
|
110
|
+
(0, vitest_1.it)('is the one the page was rendered in, on any host', async () => {
|
|
111
|
+
(0, appMode_js_1.keepRenderedMode)('preview');
|
|
112
|
+
(0, vitest_1.expect)(await sessionUrl()).toContain('mode=preview');
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)("is the request's own on the server, where there's no page", async () => {
|
|
115
|
+
vitest_1.vi.stubGlobal('window', undefined);
|
|
116
|
+
const store = new node_async_hooks_1.AsyncLocalStorage();
|
|
117
|
+
globalThis[Symbol.for('zitejs:host-context')] = store;
|
|
118
|
+
try {
|
|
119
|
+
(0, vitest_1.expect)(await store.run({ mode: 'validate' }, sessionUrl)).toContain('mode=preview');
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
delete globalThis[Symbol.for('zitejs:host-context')];
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)('is refused on a server outside any request the platform served, never guessed as live', async () => {
|
|
126
|
+
vitest_1.vi.stubGlobal('window', undefined);
|
|
127
|
+
await (0, vitest_1.expect)((0, index_js_1.uploadFile)({ data: 'hi', filename: 'f.txt' })).rejects.toThrow(index_js_1.FileUploadError);
|
|
128
|
+
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)("is a classic app's from its hostname, as before", async () => {
|
|
131
|
+
vitest_1.vi.stubGlobal('window', { location: { hostname: 'zite-editor-app_123.zite.so' } });
|
|
132
|
+
(0, vitest_1.expect)(await sessionUrl()).toContain('mode=preview');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Classic's upload client reads this too, so it imports nothing of Start: the
|
|
2
|
+
// request's host comes through the key src/start/host.ts reads it under.
|
|
3
|
+
const HOST_STORE_KEY = Symbol.for("zitejs:host-context");
|
|
4
|
+
// What a Start page was rendered with, kept in the browser when it hydrates.
|
|
5
|
+
const RENDERED_MODE = Symbol.for("zitejs:rendered-mode");
|
|
6
|
+
export const keepRenderedMode = (mode) => {
|
|
7
|
+
globalThis[RENDERED_MODE] = mode;
|
|
8
|
+
};
|
|
9
|
+
/** The mode the platform served this request or page in; undefined outside a Start app. */
|
|
10
|
+
export const platformMode = () => globalThis[HOST_STORE_KEY]?.getStore()?.mode ?? globalThis[RENDERED_MODE];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a call on an org's own AI key used, read from the provider's answer as
|
|
3
|
+
* classic's runtime reads it. For analytics only: the org pays the provider.
|
|
4
|
+
* OpenAI and Anthropic; the host meters Zite's own key itself.
|
|
5
|
+
*/
|
|
6
|
+
export type AiProvider = "openai" | "anthropic";
|
|
7
|
+
export type AiUsageRecord = {
|
|
8
|
+
provider: AiProvider;
|
|
9
|
+
model?: string;
|
|
10
|
+
inputTokens?: number;
|
|
11
|
+
outputTokens?: number;
|
|
12
|
+
cachedReadInputTokens?: number;
|
|
13
|
+
cacheCreationInputTokens?: number;
|
|
14
|
+
isStreaming?: true;
|
|
15
|
+
};
|
|
16
|
+
export declare const AI_USAGE_HOSTS: Record<string, AiProvider>;
|
|
17
|
+
/** As classic: an OpenAI chat stream reports its usage only when asked to. The body to send instead, if any. */
|
|
18
|
+
export declare const withStreamUsage: (url: URL, method: string, body: unknown) => string | undefined;
|
|
19
|
+
/** The response to hand the app, and its usage once read; a stream is split so the app reads one copy as it arrives. */
|
|
20
|
+
export declare const meterAiResponse: (provider: AiProvider, response: Response) => {
|
|
21
|
+
response: Response;
|
|
22
|
+
usage: Promise<AiUsageRecord | undefined>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a call on an org's own AI key used, read from the provider's answer as
|
|
3
|
+
* classic's runtime reads it. For analytics only: the org pays the provider.
|
|
4
|
+
* OpenAI and Anthropic; the host meters Zite's own key itself.
|
|
5
|
+
*/
|
|
6
|
+
export const AI_USAGE_HOSTS = {
|
|
7
|
+
"api.openai.com": "openai",
|
|
8
|
+
"api.anthropic.com": "anthropic",
|
|
9
|
+
};
|
|
10
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
12
|
+
/** As classic: an OpenAI chat stream reports its usage only when asked to. The body to send instead, if any. */
|
|
13
|
+
export const withStreamUsage = (url, method, body) => {
|
|
14
|
+
if (method !== "POST" || typeof body !== "string" || !url.pathname.endsWith("/chat/completions"))
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const json = JSON.parse(body);
|
|
18
|
+
if (!isRecord(json) || json.stream !== true)
|
|
19
|
+
return undefined;
|
|
20
|
+
const options = isRecord(json.stream_options) ? json.stream_options : {};
|
|
21
|
+
if (options.include_usage === true)
|
|
22
|
+
return undefined;
|
|
23
|
+
return JSON.stringify({ ...json, stream_options: { ...options, include_usage: true } });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
// OpenAI counts cached tokens inside its input; classic takes them apart.
|
|
30
|
+
const tokensFromJson = (provider, body) => {
|
|
31
|
+
const tokens = {};
|
|
32
|
+
if (!isRecord(body))
|
|
33
|
+
return tokens;
|
|
34
|
+
if (provider === "openai") {
|
|
35
|
+
const source = isRecord(body.response) ? body.response : body;
|
|
36
|
+
if (typeof source.model === "string")
|
|
37
|
+
tokens.model = source.model;
|
|
38
|
+
const usage = isRecord(source.usage) ? source.usage : undefined;
|
|
39
|
+
if (!usage)
|
|
40
|
+
return tokens;
|
|
41
|
+
const details = (name) => (isRecord(usage[name]) ? usage[name] : {});
|
|
42
|
+
const cached = details("prompt_tokens_details").cached_tokens ?? details("input_tokens_details").cached_tokens;
|
|
43
|
+
const input = usage.prompt_tokens ?? usage.input_tokens;
|
|
44
|
+
const output = usage.completion_tokens ?? usage.output_tokens;
|
|
45
|
+
if (isNumber(input))
|
|
46
|
+
tokens.inputTokens = input - (isNumber(cached) ? cached : 0);
|
|
47
|
+
if (isNumber(output))
|
|
48
|
+
tokens.outputTokens = output;
|
|
49
|
+
if (isNumber(cached))
|
|
50
|
+
tokens.cachedReadInputTokens = cached;
|
|
51
|
+
return tokens;
|
|
52
|
+
}
|
|
53
|
+
// A stream splits these: message_start has the input, message_delta the running output.
|
|
54
|
+
const message = isRecord(body.message) ? body.message : body;
|
|
55
|
+
if (typeof message.model === "string")
|
|
56
|
+
tokens.model = message.model;
|
|
57
|
+
const usage = isRecord(message.usage) ? message.usage : isRecord(body.usage) ? body.usage : undefined;
|
|
58
|
+
if (!usage)
|
|
59
|
+
return tokens;
|
|
60
|
+
if (isNumber(usage.input_tokens))
|
|
61
|
+
tokens.inputTokens = usage.input_tokens;
|
|
62
|
+
if (isNumber(usage.output_tokens))
|
|
63
|
+
tokens.outputTokens = usage.output_tokens;
|
|
64
|
+
if (isNumber(usage.cache_read_input_tokens))
|
|
65
|
+
tokens.cachedReadInputTokens = usage.cache_read_input_tokens;
|
|
66
|
+
if (isNumber(usage.cache_creation_input_tokens))
|
|
67
|
+
tokens.cacheCreationInputTokens = usage.cache_creation_input_tokens;
|
|
68
|
+
return tokens;
|
|
69
|
+
};
|
|
70
|
+
// Later readings win: Anthropic's output is cumulative, OpenAI's usage comes last.
|
|
71
|
+
const merge = (into, next) => ({
|
|
72
|
+
model: next.model ?? into.model,
|
|
73
|
+
inputTokens: next.inputTokens ?? into.inputTokens,
|
|
74
|
+
outputTokens: next.outputTokens ?? into.outputTokens,
|
|
75
|
+
cachedReadInputTokens: next.cachedReadInputTokens ?? into.cachedReadInputTokens,
|
|
76
|
+
cacheCreationInputTokens: next.cacheCreationInputTokens ?? into.cacheCreationInputTokens,
|
|
77
|
+
});
|
|
78
|
+
const MAX_PENDING_LINE = 1024 * 1024;
|
|
79
|
+
const tokensFromStream = async (provider, stream) => {
|
|
80
|
+
const reader = stream.getReader();
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let pending = "";
|
|
83
|
+
let tokens = {};
|
|
84
|
+
const readLine = (line) => {
|
|
85
|
+
if (!line.startsWith("data:"))
|
|
86
|
+
return;
|
|
87
|
+
const payload = line.slice(5).trim();
|
|
88
|
+
if (!payload || payload === "[DONE]")
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
tokens = merge(tokens, tokensFromJson(provider, JSON.parse(payload)));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not JSON; nothing to count.
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
for (;;) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done)
|
|
101
|
+
break;
|
|
102
|
+
pending += decoder.decode(value, { stream: true });
|
|
103
|
+
const lines = pending.split("\n");
|
|
104
|
+
pending = lines.pop() ?? "";
|
|
105
|
+
if (pending.length > MAX_PENDING_LINE)
|
|
106
|
+
pending = "";
|
|
107
|
+
lines.forEach(readLine);
|
|
108
|
+
}
|
|
109
|
+
readLine(pending);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// What was read before the stream broke still counts.
|
|
113
|
+
}
|
|
114
|
+
return tokens;
|
|
115
|
+
};
|
|
116
|
+
const counted = (tokens) => tokens.inputTokens !== undefined || tokens.outputTokens !== undefined;
|
|
117
|
+
const usageOf = (provider, tokens, streaming) => counted(tokens)
|
|
118
|
+
? {
|
|
119
|
+
provider,
|
|
120
|
+
...Object.fromEntries(Object.entries(tokens).filter(([, value]) => value !== undefined)),
|
|
121
|
+
...(streaming && { isStreaming: true }),
|
|
122
|
+
}
|
|
123
|
+
: undefined;
|
|
124
|
+
/** The response to hand the app, and its usage once read; a stream is split so the app reads one copy as it arrives. */
|
|
125
|
+
export const meterAiResponse = (provider, response) => {
|
|
126
|
+
const type = response.headers.get("content-type") ?? "";
|
|
127
|
+
if (!response.ok || !response.body)
|
|
128
|
+
return { response, usage: Promise.resolve(undefined) };
|
|
129
|
+
if (type.includes("text/event-stream")) {
|
|
130
|
+
const [forApp, forUs] = response.body.tee();
|
|
131
|
+
return {
|
|
132
|
+
response: new Response(forApp, response),
|
|
133
|
+
usage: tokensFromStream(provider, forUs).then((tokens) => usageOf(provider, tokens, true)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!type.includes("json"))
|
|
137
|
+
return { response, usage: Promise.resolve(undefined) };
|
|
138
|
+
return {
|
|
139
|
+
response,
|
|
140
|
+
usage: response
|
|
141
|
+
.clone()
|
|
142
|
+
.json()
|
|
143
|
+
.then((json) => usageOf(provider, tokensFromJson(provider, json), false))
|
|
144
|
+
.catch(() => undefined),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { type AirtableSettings } from "./dispatch.js";
|
|
2
2
|
import type { PlatformSession, ZiteRequestContext } from "../types.js";
|
|
3
|
-
/** The build's Airtable integration `integrationId`, or undefined when it isn't one
|
|
3
|
+
/** The build's Airtable integration `integrationId`, or undefined when it isn't one. */
|
|
4
4
|
export declare const airtableSettingsOf: (session: PlatformSession, integrationId: string) => Promise<AirtableSettings | undefined>;
|
|
5
|
-
/**
|
|
6
|
-
* One generated Airtable table call, run here with the connection's placeholder
|
|
7
|
-
* token; egress swaps the real one in for Airtable alone, and refreshes it.
|
|
8
|
-
*/
|
|
5
|
+
/** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
|
|
9
6
|
export declare function runAirtableCall(host: ZiteRequestContext, session: PlatformSession, integrationId: string, settings: AirtableSettings, methodName: string, params: unknown, access: "signedIn" | "anyone" | "webhook" | "workflow"): Promise<unknown>;
|
|
10
7
|
/** For tests: forget what this isolate learned. */
|
|
11
8
|
export declare const resetAirtableSettings: () => void;
|
|
@@ -1,44 +1,28 @@
|
|
|
1
1
|
import { executeAirtableCall } from "./dispatch.js";
|
|
2
|
+
import { INTEGRATIONS_PLATFORM_ID } from "../credentials.js";
|
|
2
3
|
import { ZiteError, isZiteErrorCode } from "../errors.js";
|
|
4
|
+
import { rememberConnection } from "../outbound.js";
|
|
3
5
|
const AIRTABLE_API = "https://api.airtable.com";
|
|
4
|
-
// The platform id `integrations.get` is answered under (`INTEGRATIONS_SDK_INTEGRATION_ID`).
|
|
5
|
-
const CONNECTIONS = "__connections__";
|
|
6
6
|
// A build's integrations don't change, so per isolate: only answers, never a pending call.
|
|
7
7
|
const settingsHeld = new Map();
|
|
8
|
-
|
|
9
|
-
const NOT_IMPLEMENTED = /does not implement (?:the )?method/i;
|
|
10
|
-
/** The build's Airtable integration `integrationId`, or undefined when it isn't one, or the host can't say. */
|
|
8
|
+
/** The build's Airtable integration `integrationId`, or undefined when it isn't one. */
|
|
11
9
|
export const airtableSettingsOf = async (session, integrationId) => {
|
|
12
|
-
if (unsupported || !session.integrationSettings)
|
|
13
|
-
return undefined;
|
|
14
10
|
if (settingsHeld.has(integrationId))
|
|
15
11
|
return settingsHeld.get(integrationId) ?? undefined;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return settings ?? undefined;
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error instanceof Error && NOT_IMPLEMENTED.test(error.message)) {
|
|
26
|
-
unsupported = true;
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
12
|
+
const answer = await session.integrationSettings(integrationId);
|
|
13
|
+
const settings = answer?.serviceType === "airtable" && typeof answer.baseId === "string"
|
|
14
|
+
? { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null }
|
|
15
|
+
: null;
|
|
16
|
+
settingsHeld.set(integrationId, settings);
|
|
17
|
+
return settings ?? undefined;
|
|
31
18
|
};
|
|
32
|
-
/**
|
|
33
|
-
* One generated Airtable table call, run here with the connection's placeholder
|
|
34
|
-
* token; egress swaps the real one in for Airtable alone, and refreshes it.
|
|
35
|
-
*/
|
|
19
|
+
/** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
|
|
36
20
|
export async function runAirtableCall(host, session, integrationId, settings, methodName, params, access) {
|
|
37
21
|
// As classic: a demo reaches no connected account.
|
|
38
22
|
if (host.mode === "demo")
|
|
39
23
|
throw new ZiteError("FORBIDDEN", "This action is not available in demo mode");
|
|
40
24
|
const connection = await session.call({
|
|
41
|
-
integrationId:
|
|
25
|
+
integrationId: INTEGRATIONS_PLATFORM_ID,
|
|
42
26
|
className: "Connections",
|
|
43
27
|
methodName: "get",
|
|
44
28
|
params: { name: integrationId },
|
|
@@ -52,6 +36,7 @@ export async function runAirtableCall(host, session, integrationId, settings, me
|
|
|
52
36
|
if (typeof token !== "string") {
|
|
53
37
|
throw new ZiteError("INTERNAL_ERROR", `The ${integrationId} connection has no token`);
|
|
54
38
|
}
|
|
39
|
+
rememberConnection(host, integrationId, connection.value);
|
|
55
40
|
host.airtableReads ??= new Map();
|
|
56
41
|
return executeAirtableCall({
|
|
57
42
|
settings,
|
|
@@ -66,5 +51,4 @@ export async function runAirtableCall(host, session, integrationId, settings, me
|
|
|
66
51
|
/** For tests: forget what this isolate learned. */
|
|
67
52
|
export const resetAirtableSettings = () => {
|
|
68
53
|
settingsHeld.clear();
|
|
69
|
-
unsupported = false;
|
|
70
54
|
};
|
|
@@ -6,11 +6,8 @@ type Found = {
|
|
|
6
6
|
baseId: string | null;
|
|
7
7
|
nameMappings: DataNameMappings | null;
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* dispatch, whose `databases` platform call still answers.
|
|
12
|
-
*/
|
|
13
|
-
export declare const dataContextOf: (session: PlatformSession) => Promise<Found | undefined>;
|
|
9
|
+
/** The build's base and SDK names, asked of the platform once per isolate. */
|
|
10
|
+
export declare const dataContextOf: (session: PlatformSession) => Promise<Found>;
|
|
14
11
|
/** One `zite.*` call, translated here and sent to base-runner through the platform. */
|
|
15
12
|
export declare function runDataCall(session: PlatformSession, found: Found, mode: HostMode, methodName: string, sent: unknown): Promise<unknown>;
|
|
16
13
|
/** For tests: forget what this isolate learned. */
|
|
@@ -8,7 +8,6 @@ const TABLES_REUSE_MS = 60_000;
|
|
|
8
8
|
// Only answers are kept, never a pending call: one request's RPC session mustn't hold up another's.
|
|
9
9
|
let context;
|
|
10
10
|
let tables;
|
|
11
|
-
let unsupported = false;
|
|
12
11
|
const tablesOf = async (baseRunner, baseId) => {
|
|
13
12
|
if (tables?.baseId === baseId && Date.now() - tables.at < TABLES_REUSE_MS)
|
|
14
13
|
return tables.tables;
|
|
@@ -16,29 +15,13 @@ const tablesOf = async (baseRunner, baseId) => {
|
|
|
16
15
|
tables = { baseId, tables: base.tables, at: Date.now() };
|
|
17
16
|
return base.tables;
|
|
18
17
|
};
|
|
19
|
-
|
|
20
|
-
const NOT_IMPLEMENTED = /does not implement (?:the )?method/i;
|
|
21
|
-
/**
|
|
22
|
-
* The build's data context, or undefined from a host that predates the app's own
|
|
23
|
-
* dispatch, whose `databases` platform call still answers.
|
|
24
|
-
*/
|
|
18
|
+
/** The build's base and SDK names, asked of the platform once per isolate. */
|
|
25
19
|
export const dataContextOf = async (session) => {
|
|
26
|
-
if (unsupported || !session.dataContext || !session.dataFetch)
|
|
27
|
-
return undefined;
|
|
28
20
|
if (context)
|
|
29
21
|
return context;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return context;
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error instanceof Error && NOT_IMPLEMENTED.test(error.message)) {
|
|
37
|
-
unsupported = true;
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
22
|
+
const answer = await session.dataContext();
|
|
23
|
+
context = { baseId: answer.baseId, nameMappings: answer.nameMappings ?? null };
|
|
24
|
+
return context;
|
|
42
25
|
};
|
|
43
26
|
/**
|
|
44
27
|
* The table id classic's generated client sends: its SDK name mapped through the
|
|
@@ -72,5 +55,4 @@ export async function runDataCall(session, found, mode, methodName, sent) {
|
|
|
72
55
|
export const resetDataDispatch = () => {
|
|
73
56
|
context = undefined;
|
|
74
57
|
tables = undefined;
|
|
75
|
-
unsupported = false;
|
|
76
58
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every fetch an app makes on the server, wrapped as classic's runtime wraps
|
|
3
|
+
* it: logged, every secret it knows redacted, with the connection whose token
|
|
4
|
+
* it carries and what an org's own AI key used; a 401 on a connection's token
|
|
5
|
+
* refreshed and sent once more; and a call on Zite's own AI key sent to the
|
|
6
|
+
* host over the app's platform binding.
|
|
7
|
+
*/
|
|
8
|
+
import type { ZiteRequestContext } from "./types.js";
|
|
9
|
+
/** An `integrations.get` answer, whose values name the calls that carry them. */
|
|
10
|
+
export declare const rememberConnection: (host: ZiteRequestContext, name: string, connection: unknown) => void;
|
|
11
|
+
export declare const createOutboundFetch: (nativeFetch: typeof fetch) => typeof fetch;
|