@q9labsai/chalk-react-native 4.1.5 → 4.1.8
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/android.js +12 -2
- package/dist/chunk-4L5GXJ36.js +58 -0
- package/dist/{chunk-HXYIJ75K.js → chunk-LZ67SXGB.js} +148 -1
- package/dist/{chunk-5HETUU73.js → chunk-MMBE2BVK.js} +11 -2
- package/dist/client.js +1 -1
- package/dist/diagnostics.d.ts +4 -4
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +7 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/invites.d.ts +6 -1
- package/dist/invites.d.ts.map +1 -1
- package/dist/invites.js +8 -39
- package/dist/ios-pad.js +12 -2
- package/dist/ios-phone.js +12 -2
- package/dist/ios.js +12 -2
- package/dist/macos.js +12 -2
- package/dist/space-client/cloudflare-rtk-native.d.ts +3 -0
- package/dist/space-client/cloudflare-rtk-native.d.ts.map +1 -0
- package/dist/space-client/create-native-space-client.d.ts.map +1 -1
- package/dist/utils/build-chalk-invite-link.d.ts +1 -1
- package/dist/utils/build-chalk-invite-link.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/diagnostics.test.ts +3 -3
- package/src/diagnostics.ts +8 -8
- package/src/index.ts +3 -0
- package/src/invites.test.ts +24 -29
- package/src/invites.ts +34 -10
- package/src/space-client/cloudflare-rtk-native.test.ts +118 -0
- package/src/space-client/cloudflare-rtk-native.ts +104 -0
- package/src/space-client/create-native-space-client.test.ts +54 -1
- package/src/space-client/create-native-space-client.ts +55 -2
- package/src/utils/build-chalk-invite-link.test.ts +3 -3
- package/src/utils/build-chalk-invite-link.ts +4 -2
package/dist/macos.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Chalk,
|
|
3
3
|
ChalkProvider,
|
|
4
4
|
Entrance,
|
|
5
|
+
buildChalkInviteLink,
|
|
5
6
|
useCan,
|
|
6
7
|
useChat,
|
|
7
8
|
useConnection,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
useSelf,
|
|
12
13
|
useSpaceClient,
|
|
13
14
|
useWhiteboard
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-MMBE2BVK.js";
|
|
15
16
|
import {
|
|
16
17
|
THEME_PALETTES,
|
|
17
18
|
THEME_TEXTURES,
|
|
@@ -23,7 +24,12 @@ import {
|
|
|
23
24
|
Theme,
|
|
24
25
|
createNativeTheme
|
|
25
26
|
} from "./chunk-JB4LNWVP.js";
|
|
26
|
-
import "./chunk-
|
|
27
|
+
import "./chunk-LZ67SXGB.js";
|
|
28
|
+
import {
|
|
29
|
+
extractSpaceInviteFromLink,
|
|
30
|
+
getClipboardInviteSuggestion,
|
|
31
|
+
parseSpaceInviteLink
|
|
32
|
+
} from "./chunk-4L5GXJ36.js";
|
|
27
33
|
export {
|
|
28
34
|
Chalk,
|
|
29
35
|
ChalkProvider,
|
|
@@ -31,9 +37,13 @@ export {
|
|
|
31
37
|
THEME_PALETTES,
|
|
32
38
|
THEME_TEXTURES,
|
|
33
39
|
Theme,
|
|
40
|
+
buildChalkInviteLink,
|
|
34
41
|
createNativeTheme,
|
|
42
|
+
extractSpaceInviteFromLink,
|
|
43
|
+
getClipboardInviteSuggestion,
|
|
35
44
|
getThemeMode,
|
|
36
45
|
isDarkThemePalette,
|
|
46
|
+
parseSpaceInviteLink,
|
|
37
47
|
resolveNativeAppearance,
|
|
38
48
|
useCan,
|
|
39
49
|
useChat,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-rtk-native.d.ts","sourceRoot":"","sources":["../../src/space-client/cloudflare-rtk-native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAqD,MAAM,wBAAwB,CAAC;AAK5H,eAAO,MAAM,6BAA6B,EAAE,0BAQ3C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-native-space-client.d.ts","sourceRoot":"","sources":["../../src/space-client/create-native-space-client.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"create-native-space-client.d.ts","sourceRoot":"","sources":["../../src/space-client/create-native-space-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,SAAS,EAGd,KAAK,uBAAuB,EAC7B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,EAAwK,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAU3N,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CAgDtF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function buildChalkInviteLink(
|
|
1
|
+
export declare function buildChalkInviteLink(spaceSlug: string, spaceInviteToken: string, origin?: string): string;
|
|
2
2
|
//# sourceMappingURL=build-chalk-invite-link.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-chalk-invite-link.d.ts","sourceRoot":"","sources":["../../src/utils/build-chalk-invite-link.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,SAAuB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"build-chalk-invite-link.d.ts","sourceRoot":"","sources":["../../src/utils/build-chalk-invite-link.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,SAAuB,GAAG,MAAM,CAIvH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q9labsai/chalk-react-native",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -150,9 +150,9 @@
|
|
|
150
150
|
"registry": "https://registry.npmjs.org/"
|
|
151
151
|
},
|
|
152
152
|
"dependencies": {
|
|
153
|
-
"@q9labsai/chalk-client": "^4.1.
|
|
154
|
-
"@q9labsai/chalk-whiteboard": "^4.1.
|
|
155
|
-
"@q9labsai/facehash": "^4.1.
|
|
153
|
+
"@q9labsai/chalk-client": "^4.1.8",
|
|
154
|
+
"@q9labsai/chalk-whiteboard": "^4.1.8",
|
|
155
|
+
"@q9labsai/facehash": "^4.1.8"
|
|
156
156
|
},
|
|
157
157
|
"devDependencies": {
|
|
158
158
|
"@types/node": "^26.2.0",
|
|
@@ -162,6 +162,7 @@
|
|
|
162
162
|
},
|
|
163
163
|
"peerDependencies": {
|
|
164
164
|
"@cloudflare/react-native-webrtc": "~125.0.1 || ~125.0.3",
|
|
165
|
+
"@cloudflare/realtimekit-react-native": "1.1.0",
|
|
165
166
|
"@hugeicons/core-free-icons": "^4.0.0",
|
|
166
167
|
"@hugeicons/react-native": "^1.0.13",
|
|
167
168
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -170,9 +171,9 @@
|
|
|
170
171
|
"react-native-webview": "^13.16.1"
|
|
171
172
|
},
|
|
172
173
|
"scripts": {
|
|
173
|
-
"build": "rm -rf ./dist && tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc && pnpm run build:types && pnpm run sync:whiteboard-assets",
|
|
174
|
+
"build": "rm -rf ./dist && tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native && pnpm run build:types && pnpm run sync:whiteboard-assets",
|
|
174
175
|
"build:types": "rm -f tsconfig.tsbuildinfo && tsc --build tsconfig.json",
|
|
175
|
-
"dev": "tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --watch",
|
|
176
|
+
"dev": "tsup ./src/index.ts ./src/client.ts ./src/android.ts ./src/ios.ts ./src/ios-phone.ts ./src/ios-pad.ts ./src/macos.ts ./src/runtime.ts ./src/clipboard.ts ./src/diagnostics.ts ./src/invites.ts ./src/ui/theme.ts ./src/ui/appearance.ts --format esm --out-dir ./dist --platform browser --external react --external react-native --external react-native-webview --external @q9labsai/chalk-whiteboard --external @cloudflare/react-native-webrtc --external @cloudflare/realtimekit-react-native --watch",
|
|
176
177
|
"sync:whiteboard-assets": "node ./scripts/sync-whiteboard-assets.mjs",
|
|
177
178
|
"test": "pnpm exec vitest run",
|
|
178
179
|
"check-types": "tsc --project tsconfig.check-types.json --noEmit",
|
package/src/diagnostics.test.ts
CHANGED
|
@@ -9,12 +9,12 @@ describe("native diagnostics", () => {
|
|
|
9
9
|
expect(buildDevDiagnosticsCopyText()).not.toContain("inviteToken");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it("uses the
|
|
12
|
+
it("uses the public-invite API as the single environment boundary", () => {
|
|
13
13
|
setDevDiagnosticsEnvironment({
|
|
14
|
-
|
|
14
|
+
apiBaseURL: "http://localhost:8787",
|
|
15
15
|
});
|
|
16
16
|
expect(getDevDiagnosticsState().environment.target).toBe("local");
|
|
17
|
-
expect(classifyTarget("https://chalkmeet.com
|
|
17
|
+
expect(classifyTarget("https://api.chalkmeet.com")).toBe("production");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it("records failures without legacy auth or transport state", () => {
|
package/src/diagnostics.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface DevDiagnosticsTimelineEntry {
|
|
|
17
17
|
export interface DevDiagnosticsState {
|
|
18
18
|
readonly enabled: boolean;
|
|
19
19
|
readonly environment: {
|
|
20
|
+
readonly apiBaseURL: string | null;
|
|
20
21
|
readonly buildProfile: string | null;
|
|
21
|
-
readonly brokerUrl: string | null;
|
|
22
22
|
readonly target: "custom" | "local" | "production" | "unknown";
|
|
23
23
|
readonly routeKind: string | null;
|
|
24
24
|
readonly routeSpaceId: string | null;
|
|
@@ -40,10 +40,10 @@ const maximumTimelineItems = 120;
|
|
|
40
40
|
const listeners = new Set<() => void>();
|
|
41
41
|
let state = initialState();
|
|
42
42
|
|
|
43
|
-
export function classifyTarget(
|
|
44
|
-
if (!
|
|
43
|
+
export function classifyTarget(apiBaseURL: string | null | undefined): DevDiagnosticsState["environment"]["target"] {
|
|
44
|
+
if (!apiBaseURL) return "unknown";
|
|
45
45
|
try {
|
|
46
|
-
const hostname = new URL(
|
|
46
|
+
const hostname = new URL(apiBaseURL).hostname;
|
|
47
47
|
if (["localhost", "127.0.0.1", "0.0.0.0"].includes(hostname)) return "local";
|
|
48
48
|
if (hostname === "chalkmeet.com" || hostname.endsWith(".chalkmeet.com")) return "production";
|
|
49
49
|
return "custom";
|
|
@@ -52,13 +52,13 @@ export function classifyTarget(brokerUrl: string | null | undefined): DevDiagnos
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function resolveDevDiagnosticsMode({ isDevRuntime,
|
|
55
|
+
export function resolveDevDiagnosticsMode({ isDevRuntime, apiBaseURL }: { readonly isDevRuntime: boolean; readonly apiBaseURL: string | null | undefined }): {
|
|
56
56
|
readonly enabled: boolean;
|
|
57
57
|
readonly buildProfile: "development" | "production";
|
|
58
58
|
} {
|
|
59
59
|
return {
|
|
60
60
|
enabled: isDevRuntime,
|
|
61
|
-
buildProfile: isDevRuntime || classifyTarget(
|
|
61
|
+
buildProfile: isDevRuntime || classifyTarget(apiBaseURL) === "local" ? "development" : "production",
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -77,7 +77,7 @@ export function setDevDiagnosticsEnvironment(next: Partial<DevDiagnosticsState["
|
|
|
77
77
|
environment: {
|
|
78
78
|
...current.environment,
|
|
79
79
|
...next,
|
|
80
|
-
target: next.
|
|
80
|
+
target: next.apiBaseURL ? classifyTarget(next.apiBaseURL) : current.environment.target,
|
|
81
81
|
},
|
|
82
82
|
}));
|
|
83
83
|
}
|
|
@@ -145,8 +145,8 @@ function initialState(): DevDiagnosticsState {
|
|
|
145
145
|
return {
|
|
146
146
|
enabled: isDevelopmentRuntime,
|
|
147
147
|
environment: {
|
|
148
|
+
apiBaseURL: null,
|
|
148
149
|
buildProfile: null,
|
|
149
|
-
brokerUrl: null,
|
|
150
150
|
target: "unknown",
|
|
151
151
|
routeKind: null,
|
|
152
152
|
routeSpaceId: null,
|
package/src/index.ts
CHANGED
|
@@ -6,5 +6,8 @@ export { ChalkProvider, useSpaceClient } from "./context/space-client-context";
|
|
|
6
6
|
export type { ChalkProviderProps } from "./context/space-client-context";
|
|
7
7
|
export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useSelf, useWhiteboard } from "./hooks/space-hooks";
|
|
8
8
|
export { Theme, createNativeTheme } from "./ui/theme";
|
|
9
|
+
export { extractSpaceInviteFromLink, getClipboardInviteSuggestion, parseSpaceInviteLink } from "./invites";
|
|
10
|
+
export type { SpaceInviteLink } from "./invites";
|
|
11
|
+
export { buildChalkInviteLink } from "./utils/build-chalk-invite-link";
|
|
9
12
|
export { getThemeMode, isDarkThemePalette, resolveNativeAppearance, THEME_PALETTES, THEME_TEXTURES } from "./ui/appearance";
|
|
10
13
|
export type { NativeAppearance, NativeAppearanceTokens, NativeShadowToken, NativeTextureBlendMode, NativeTextureDescriptor, NativeTextureKind, ThemeAppearance, ThemeMode, ThemePalette, ThemeTexture } from "./ui/appearance";
|
package/src/invites.test.ts
CHANGED
|
@@ -1,53 +1,48 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { getClipboardInviteSuggestion, parseSpaceInviteLink } from "./invites";
|
|
3
3
|
|
|
4
|
-
describe("
|
|
5
|
-
const inviteToken = "
|
|
4
|
+
describe("parseSpaceInviteLink", () => {
|
|
5
|
+
const inviteToken = "cspi1.key.payload.signature";
|
|
6
6
|
|
|
7
7
|
it("accepts Chalk https invite links", () => {
|
|
8
|
-
expect(
|
|
8
|
+
expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
it("accepts
|
|
12
|
-
expect(
|
|
11
|
+
it("accepts bare Chalk hostnames", () => {
|
|
12
|
+
expect(parseSpaceInviteLink(`chalkmeet.com/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
it("accepts the canonical
|
|
16
|
-
expect(
|
|
15
|
+
it("accepts native Chalk deep links with the canonical path", () => {
|
|
16
|
+
expect(parseSpaceInviteLink(`chalk://space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
17
|
+
expect(parseSpaceInviteLink(`ai.q9labs.chalk.mobile://space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
it("accepts
|
|
20
|
-
expect(
|
|
20
|
+
it("accepts loopback HTTP invite links for local development", () => {
|
|
21
|
+
expect(parseSpaceInviteLink(`http://127.0.0.1:3070/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
22
|
+
expect(parseSpaceInviteLink(`http://localhost:3070/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
|
|
21
23
|
});
|
|
22
24
|
|
|
23
|
-
it("
|
|
24
|
-
expect(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
expect(extractJoinTokenFromInviteLink(`ai.q9labs.chalk.mobile://j/${inviteToken}`)).toBe(inviteToken);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("rejects direct Space links and raw codes", () => {
|
|
36
|
-
expect(extractJoinTokenFromInviteLink("https://chalk.q9labs.ai/space/2f0b302b-2449-43f5-ae3b-de57decb9f09")).toBeNull();
|
|
37
|
-
expect(extractJoinTokenFromInviteLink(`https://attacker.test/j/${inviteToken}`)).toBeNull();
|
|
38
|
-
expect(extractJoinTokenFromInviteLink("ABC123")).toBeNull();
|
|
25
|
+
it("rejects legacy paths, missing slugs, and invalid tokens", () => {
|
|
26
|
+
expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/legacy/${inviteToken}`)).toBeNull();
|
|
27
|
+
expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space#spaceInviteToken=${inviteToken}`)).toBeNull();
|
|
28
|
+
expect(parseSpaceInviteLink("https://chalk.q9labs.ai/space/design-lab#invite=legacy")).toBeNull();
|
|
29
|
+
expect(parseSpaceInviteLink("https://attacker.test/space/design-lab#spaceInviteToken=cspi1.key.payload.signature")).toBeNull();
|
|
30
|
+
expect(parseSpaceInviteLink(`http://chalkmeet.com/space/design-lab#spaceInviteToken=${inviteToken}`)).toBeNull();
|
|
31
|
+
expect(parseSpaceInviteLink(`http://192.168.1.10/space/design-lab#spaceInviteToken=${inviteToken}`)).toBeNull();
|
|
32
|
+
expect(parseSpaceInviteLink("https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=ABC123")).toBeNull();
|
|
33
|
+
expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=${`cspi1.${"k".repeat(170)}.${"p".repeat(170)}.${"s".repeat(170)}`}`)).toBeNull();
|
|
39
34
|
});
|
|
40
35
|
});
|
|
41
36
|
|
|
42
37
|
describe("getClipboardInviteSuggestion", () => {
|
|
43
|
-
const
|
|
38
|
+
const inviteLink = "https://chalkmeet.com/space/design-lab#spaceInviteToken=cspi1.key.payload.signature";
|
|
44
39
|
|
|
45
40
|
it("returns a copied Chalk invite link when it differs from the current input", () => {
|
|
46
|
-
expect(getClipboardInviteSuggestion(
|
|
41
|
+
expect(getClipboardInviteSuggestion(inviteLink, "")).toBe(inviteLink);
|
|
47
42
|
});
|
|
48
43
|
|
|
49
44
|
it("hides the suggestion when the input already matches the clipboard", () => {
|
|
50
|
-
expect(getClipboardInviteSuggestion(
|
|
45
|
+
expect(getClipboardInviteSuggestion(inviteLink, inviteLink)).toBeNull();
|
|
51
46
|
});
|
|
52
47
|
|
|
53
48
|
it("ignores non-invite clipboard values", () => {
|
package/src/invites.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
const CHALK_INVITE_HOSTS = ["chalkmeet.com", "chalk.q9labs.ai"]
|
|
1
|
+
const CHALK_INVITE_HOSTS: readonly string[] = ["chalkmeet.com", "chalk.q9labs.ai"];
|
|
2
2
|
const CHALK_CUSTOM_PROTOCOLS = new Set(["chalk:", "ai.q9labs.chalk.mobile:"]);
|
|
3
|
-
const
|
|
3
|
+
const SPACE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
4
|
+
const SPACE_INVITE_TOKEN_PATTERN = /^cspi1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u;
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export type SpaceInviteLink = {
|
|
7
|
+
readonly slug: string;
|
|
8
|
+
readonly spaceInviteToken: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function parseSpaceInviteLink(input: string): SpaceInviteLink | null {
|
|
6
12
|
const trimmed = input.trim();
|
|
7
13
|
if (!trimmed) {
|
|
8
14
|
return null;
|
|
@@ -15,20 +21,38 @@ export function extractJoinTokenFromInviteLink(input: string): string | null {
|
|
|
15
21
|
|
|
16
22
|
try {
|
|
17
23
|
const parsed = new URL(normalized);
|
|
18
|
-
if (!CHALK_CUSTOM_PROTOCOLS.has(parsed.protocol) &&
|
|
24
|
+
if (!CHALK_CUSTOM_PROTOCOLS.has(parsed.protocol) && !isAllowedWebInviteOrigin(parsed)) {
|
|
19
25
|
return null;
|
|
20
26
|
}
|
|
21
|
-
const pathSegments =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const token = head === "j" && tail ? tail : hashInvite?.trim();
|
|
27
|
+
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
28
|
+
if (CHALK_CUSTOM_PROTOCOLS.has(parsed.protocol)) pathSegments.unshift(parsed.hostname);
|
|
29
|
+
if (pathSegments.length !== 2 || pathSegments[0] !== "space") return null;
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
const slug = pathSegments[1];
|
|
32
|
+
const token = new URLSearchParams(parsed.hash.replace(/^#/u, "")).get("spaceInviteToken")?.trim();
|
|
33
|
+
return slug && token && token.length <= 512 && SPACE_SLUG_PATTERN.test(slug) && SPACE_INVITE_TOKEN_PATTERN.test(token) ? { slug, spaceInviteToken: token } : null;
|
|
27
34
|
} catch {
|
|
28
35
|
return null;
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
function isChalkInviteHost(hostname: string): boolean {
|
|
40
|
+
return CHALK_INVITE_HOSTS.some((host) => host === hostname);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isAllowedWebInviteOrigin(url: URL): boolean {
|
|
44
|
+
if (url.protocol === "https:") return isChalkInviteHost(url.hostname);
|
|
45
|
+
return url.protocol === "http:" && isLoopbackHost(url.hostname);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
49
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function extractSpaceInviteFromLink(input: string): SpaceInviteLink | null {
|
|
53
|
+
return parseSpaceInviteLink(input);
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
export function getClipboardInviteSuggestion(clipboardText: string | null | undefined, currentInput = ""): string | null {
|
|
33
57
|
const normalizedClipboardText = clipboardText?.trim();
|
|
34
58
|
if (!normalizedClipboardText) {
|
|
@@ -39,5 +63,5 @@ export function getClipboardInviteSuggestion(clipboardText: string | null | unde
|
|
|
39
63
|
return null;
|
|
40
64
|
}
|
|
41
65
|
|
|
42
|
-
return
|
|
66
|
+
return parseSpaceInviteLink(normalizedClipboardText) ? normalizedClipboardText : null;
|
|
43
67
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const initialize = vi.hoisted(() => vi.fn());
|
|
4
|
+
|
|
5
|
+
vi.mock("@cloudflare/realtimekit-react-native", () => ({ default: { init: initialize } }));
|
|
6
|
+
|
|
7
|
+
import { createNativeRealtimeKitClient } from "./cloudflare-rtk-native";
|
|
8
|
+
|
|
9
|
+
describe("native RealtimeKit adapter", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("initializes, joins, leaves, and projects native participants", async () => {
|
|
15
|
+
const selfOn = vi.fn();
|
|
16
|
+
const selfOff = vi.fn();
|
|
17
|
+
const joinedOn = vi.fn();
|
|
18
|
+
const joinedOff = vi.fn();
|
|
19
|
+
const joinRoom = vi.fn().mockResolvedValue(undefined);
|
|
20
|
+
const leaveRoom = vi.fn().mockResolvedValue(undefined);
|
|
21
|
+
const audioTrack = { kind: "audio" };
|
|
22
|
+
const videoTrack = { kind: "video" };
|
|
23
|
+
const participant = {
|
|
24
|
+
id: "native-participant",
|
|
25
|
+
userId: "user",
|
|
26
|
+
customParticipantId: "custom-participant",
|
|
27
|
+
audioEnabled: true,
|
|
28
|
+
videoEnabled: false,
|
|
29
|
+
screenShareEnabled: false,
|
|
30
|
+
audioTrack,
|
|
31
|
+
videoTrack: null,
|
|
32
|
+
screenShareTracks: {},
|
|
33
|
+
};
|
|
34
|
+
const nativeClient = {
|
|
35
|
+
joinRoom,
|
|
36
|
+
leaveRoom,
|
|
37
|
+
self: {
|
|
38
|
+
peerId: "peer",
|
|
39
|
+
audioEnabled: true,
|
|
40
|
+
videoEnabled: false,
|
|
41
|
+
screenShareEnabled: false,
|
|
42
|
+
audioTrack,
|
|
43
|
+
videoTrack,
|
|
44
|
+
screenShareTracks: {},
|
|
45
|
+
enableAudio: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
enableVideo: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
enableScreenShare: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
disableAudio: vi.fn().mockResolvedValue(undefined),
|
|
49
|
+
disableVideo: vi.fn().mockResolvedValue(undefined),
|
|
50
|
+
disableScreenShare: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
on: selfOn,
|
|
52
|
+
off: selfOff,
|
|
53
|
+
},
|
|
54
|
+
participants: {
|
|
55
|
+
joined: {
|
|
56
|
+
toArray: vi.fn(() => [participant]),
|
|
57
|
+
on: joinedOn,
|
|
58
|
+
off: joinedOff,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
initialize.mockResolvedValue(nativeClient);
|
|
63
|
+
|
|
64
|
+
const onError = vi.fn();
|
|
65
|
+
const connection = await createNativeRealtimeKitClient({ authToken: "rtk-token", onError });
|
|
66
|
+
await connection.join();
|
|
67
|
+
await connection.leave();
|
|
68
|
+
|
|
69
|
+
expect(initialize).toHaveBeenCalledWith({ authToken: "rtk-token", defaults: { audio: false, video: false }, onError });
|
|
70
|
+
expect(joinRoom).toHaveBeenCalledOnce();
|
|
71
|
+
expect(leaveRoom).toHaveBeenCalledOnce();
|
|
72
|
+
expect(connection.self.peerId).toBe("peer");
|
|
73
|
+
expect(connection.self.audioTrack).toBe(audioTrack);
|
|
74
|
+
expect(connection.participants.joined.list()).toEqual([expect.objectContaining({ id: "native-participant", customParticipantId: "custom-participant", audioTrack })]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("bridges lifecycle event subscriptions and removes them", async () => {
|
|
78
|
+
const selfOn = vi.fn();
|
|
79
|
+
const selfOff = vi.fn();
|
|
80
|
+
const joinedOn = vi.fn();
|
|
81
|
+
const joinedOff = vi.fn();
|
|
82
|
+
initialize.mockResolvedValue({
|
|
83
|
+
joinRoom: vi.fn(),
|
|
84
|
+
leaveRoom: vi.fn(),
|
|
85
|
+
self: {
|
|
86
|
+
peerId: "peer",
|
|
87
|
+
audioEnabled: false,
|
|
88
|
+
videoEnabled: false,
|
|
89
|
+
screenShareEnabled: false,
|
|
90
|
+
audioTrack: null,
|
|
91
|
+
videoTrack: null,
|
|
92
|
+
screenShareTracks: {},
|
|
93
|
+
enableAudio: vi.fn(),
|
|
94
|
+
enableVideo: vi.fn(),
|
|
95
|
+
enableScreenShare: vi.fn(),
|
|
96
|
+
disableAudio: vi.fn(),
|
|
97
|
+
disableVideo: vi.fn(),
|
|
98
|
+
disableScreenShare: vi.fn(),
|
|
99
|
+
on: selfOn,
|
|
100
|
+
off: selfOff,
|
|
101
|
+
},
|
|
102
|
+
participants: { joined: { toArray: vi.fn(() => []), on: joinedOn, off: joinedOff } },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const connection = await createNativeRealtimeKitClient({ authToken: "rtk-token", onError: vi.fn() });
|
|
106
|
+
const listener = vi.fn();
|
|
107
|
+
const unsubscribeAudio = connection.self.onAudioUpdate(listener);
|
|
108
|
+
const unsubscribeJoined = connection.participants.joined.onJoined(listener);
|
|
109
|
+
|
|
110
|
+
unsubscribeAudio();
|
|
111
|
+
unsubscribeJoined();
|
|
112
|
+
|
|
113
|
+
expect(selfOn).toHaveBeenCalledWith("audioUpdate", listener);
|
|
114
|
+
expect(selfOff).toHaveBeenCalledWith("audioUpdate", listener);
|
|
115
|
+
expect(joinedOn).toHaveBeenCalledWith("participantJoined", expect.any(Function));
|
|
116
|
+
expect(joinedOff).toHaveBeenCalledWith("participantJoined", expect.any(Function));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type RealtimeKitClient from "@cloudflare/realtimekit-react-native";
|
|
2
|
+
import type { CloudflareRTKClientFactory, CloudflareRTKConnection, CloudflareRTKParticipant } from "@q9labsai/chalk-client";
|
|
3
|
+
|
|
4
|
+
type NativeRealtimeKitClient = Awaited<ReturnType<typeof RealtimeKitClient.init>>;
|
|
5
|
+
type NativeRealtimeKitParticipant = ReturnType<NativeRealtimeKitClient["participants"]["joined"]["toArray"]>[number];
|
|
6
|
+
|
|
7
|
+
export const createNativeRealtimeKitClient: CloudflareRTKClientFactory = async ({ authToken, onError }) => {
|
|
8
|
+
const { default: NativeRealtimeKitClient } = await import("@cloudflare/realtimekit-react-native");
|
|
9
|
+
const client = await NativeRealtimeKitClient.init({
|
|
10
|
+
authToken,
|
|
11
|
+
defaults: { audio: false, video: false },
|
|
12
|
+
onError,
|
|
13
|
+
});
|
|
14
|
+
return adaptNativeRealtimeKitClient(client);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function adaptNativeRealtimeKitClient(client: NativeRealtimeKitClient): CloudflareRTKConnection {
|
|
18
|
+
const self = client.self;
|
|
19
|
+
return {
|
|
20
|
+
join: () => client.joinRoom(),
|
|
21
|
+
leave: () => client.leaveRoom(),
|
|
22
|
+
self: {
|
|
23
|
+
get peerId() {
|
|
24
|
+
return self.peerId;
|
|
25
|
+
},
|
|
26
|
+
get audioEnabled() {
|
|
27
|
+
return self.audioEnabled;
|
|
28
|
+
},
|
|
29
|
+
get videoEnabled() {
|
|
30
|
+
return self.videoEnabled;
|
|
31
|
+
},
|
|
32
|
+
get screenShareEnabled() {
|
|
33
|
+
return self.screenShareEnabled;
|
|
34
|
+
},
|
|
35
|
+
get audioTrack() {
|
|
36
|
+
return self.audioTrack ?? null;
|
|
37
|
+
},
|
|
38
|
+
get videoTrack() {
|
|
39
|
+
return self.videoTrack ?? null;
|
|
40
|
+
},
|
|
41
|
+
get screenShareTracks() {
|
|
42
|
+
return {
|
|
43
|
+
...(self.screenShareTracks.audio ? { audio: self.screenShareTracks.audio } : {}),
|
|
44
|
+
...(self.screenShareTracks.video ? { video: self.screenShareTracks.video } : {}),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
enableAudio: () => self.enableAudio(),
|
|
48
|
+
enableVideo: () => self.enableVideo(),
|
|
49
|
+
enableScreenShare: () => self.enableScreenShare(),
|
|
50
|
+
disableAudio: () => self.disableAudio(),
|
|
51
|
+
disableVideo: () => self.disableVideo(),
|
|
52
|
+
disableScreenShare: () => self.disableScreenShare(),
|
|
53
|
+
onAudioUpdate: (listener) => {
|
|
54
|
+
self.on("audioUpdate", listener);
|
|
55
|
+
return () => self.off("audioUpdate", listener);
|
|
56
|
+
},
|
|
57
|
+
onVideoUpdate: (listener) => {
|
|
58
|
+
self.on("videoUpdate", listener);
|
|
59
|
+
return () => self.off("videoUpdate", listener);
|
|
60
|
+
},
|
|
61
|
+
onScreenShareUpdate: (listener) => {
|
|
62
|
+
self.on("screenShareUpdate", listener);
|
|
63
|
+
return () => self.off("screenShareUpdate", listener);
|
|
64
|
+
},
|
|
65
|
+
onLeft: (listener) => {
|
|
66
|
+
const callback = () => listener();
|
|
67
|
+
self.on("roomLeft", callback);
|
|
68
|
+
return () => self.off("roomLeft", callback);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
participants: {
|
|
72
|
+
joined: {
|
|
73
|
+
list: () => client.participants.joined.toArray().map(toParticipant),
|
|
74
|
+
onJoined: (listener) => {
|
|
75
|
+
const callback = (participant: NativeRealtimeKitParticipant) => listener(toParticipant(participant));
|
|
76
|
+
client.participants.joined.on("participantJoined", callback);
|
|
77
|
+
return () => client.participants.joined.off("participantJoined", callback);
|
|
78
|
+
},
|
|
79
|
+
onLeft: (listener) => {
|
|
80
|
+
const callback = (participant: NativeRealtimeKitParticipant) => listener(toParticipant(participant));
|
|
81
|
+
client.participants.joined.on("participantLeft", callback);
|
|
82
|
+
return () => client.participants.joined.off("participantLeft", callback);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toParticipant(participant: NativeRealtimeKitParticipant): CloudflareRTKParticipant {
|
|
90
|
+
return {
|
|
91
|
+
id: participant.id,
|
|
92
|
+
userId: participant.userId,
|
|
93
|
+
...(participant.customParticipantId === undefined ? {} : { customParticipantId: participant.customParticipantId }),
|
|
94
|
+
audioEnabled: participant.audioEnabled,
|
|
95
|
+
videoEnabled: participant.videoEnabled,
|
|
96
|
+
screenShareEnabled: participant.screenShareEnabled,
|
|
97
|
+
audioTrack: participant.audioTrack ?? null,
|
|
98
|
+
videoTrack: participant.videoTrack ?? null,
|
|
99
|
+
screenShareTracks: {
|
|
100
|
+
...(participant.screenShareTracks.audio ? { audio: participant.screenShareTracks.audio } : {}),
|
|
101
|
+
...(participant.screenShareTracks.video ? { video: participant.screenShareTracks.video } : {}),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -4,10 +4,43 @@ const createSpaceClientForPlatform = vi.hoisted(() => vi.fn());
|
|
|
4
4
|
const addAppStateListener = vi.hoisted(() => vi.fn());
|
|
5
5
|
const createV1SyncClient = vi.hoisted(() => vi.fn());
|
|
6
6
|
const createCloudflareSFUHTTPTransport = vi.hoisted(() => vi.fn(() => ({})));
|
|
7
|
+
const CloudflareRTKClient = vi.hoisted(() =>
|
|
8
|
+
vi.fn(
|
|
9
|
+
class MockCloudflareRTKClient {
|
|
10
|
+
setLocalPublicationTarget = vi.fn();
|
|
11
|
+
observeLocalPublications = vi.fn();
|
|
12
|
+
observeRemotePublications = vi.fn();
|
|
13
|
+
start = vi.fn();
|
|
14
|
+
stop = vi.fn();
|
|
15
|
+
restart = vi.fn();
|
|
16
|
+
prepareLocalTrack = vi.fn();
|
|
17
|
+
clearPreparedLocalTrack = vi.fn();
|
|
18
|
+
getSnapshot = vi.fn();
|
|
19
|
+
subscribe = vi.fn();
|
|
20
|
+
},
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
const CloudflareSFUClient = vi.hoisted(() =>
|
|
24
|
+
vi.fn(
|
|
25
|
+
class MockCloudflareSFUClient {
|
|
26
|
+
setLocalPublicationTarget = vi.fn();
|
|
27
|
+
observeLocalPublications = vi.fn();
|
|
28
|
+
observeRemotePublications = vi.fn();
|
|
29
|
+
start = vi.fn();
|
|
30
|
+
stop = vi.fn();
|
|
31
|
+
restart = vi.fn();
|
|
32
|
+
prepareLocalTrack = vi.fn();
|
|
33
|
+
clearPreparedLocalTrack = vi.fn();
|
|
34
|
+
getSnapshot = vi.fn();
|
|
35
|
+
subscribe = vi.fn();
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
);
|
|
7
39
|
|
|
8
40
|
vi.mock("@q9labsai/chalk-client", () => ({
|
|
9
41
|
AsyncStorageV1PendingTargetStore: vi.fn(),
|
|
10
|
-
|
|
42
|
+
CloudflareRTKClient,
|
|
43
|
+
CloudflareSFUClient,
|
|
11
44
|
createChalkChatFileHttpTransport: vi.fn(),
|
|
12
45
|
createChalkWhiteboardV1Client: vi.fn(),
|
|
13
46
|
createChalkWhiteboardV1FileHttpTransport: vi.fn(),
|
|
@@ -58,6 +91,26 @@ describe("native client creation", () => {
|
|
|
58
91
|
expect(createSpaceClientForPlatform.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ apiBaseUrl: "https://api.chalkmeet.com", syncUrl: "wss://sync.chalkmeet.com/v1/sync" }));
|
|
59
92
|
});
|
|
60
93
|
|
|
94
|
+
it("selects the native RealtimeKit adapter for RTK access grants", () => {
|
|
95
|
+
createNativeSpaceClient(options());
|
|
96
|
+
const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as {
|
|
97
|
+
readonly dependencies: { readonly createMediaClient: (input: unknown) => unknown };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
platform.dependencies.createMediaClient({
|
|
101
|
+
access: {
|
|
102
|
+
subject: { tenantId: "tenant", spaceId: "space", episodeId: "episode", participantId: "participant", participantGeneration: 1 },
|
|
103
|
+
media: { provider: "cloudflare_rtk", clientPayload: { providerSubject: "provider-subject", token: "rtk-token" } },
|
|
104
|
+
},
|
|
105
|
+
credential: vi.fn(),
|
|
106
|
+
onFailure: vi.fn(),
|
|
107
|
+
onScreenEnded: vi.fn(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(CloudflareRTKClient).toHaveBeenCalledWith(expect.objectContaining({ authToken: "rtk-token", participantId: "participant", clientFactory: expect.any(Function) }));
|
|
111
|
+
expect(createCloudflareSFUHTTPTransport).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
61
114
|
it("passes the closed access callback to the client", () => {
|
|
62
115
|
const getAccess = vi.fn();
|
|
63
116
|
createNativeSpaceClient({ ...options(), getAccess });
|