@buoy-gg/bug-links 0.0.0-stage → 7.0.81
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/BuoyBugLinks.podspec +18 -0
- package/LICENSE +58 -0
- package/README.md +25 -2
- package/android/build.gradle +31 -0
- package/android/consumer-rules.pro +8 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/gg/buoy/buglinks/frames/BuoyFramesModule.kt +53 -0
- package/android/src/main/java/gg/buoy/buglinks/frames/BuoyFramesPackage.kt +11 -0
- package/android/src/main/java/gg/buoy/buglinks/frames/FrameEngine.kt +707 -0
- package/android/src/main/java/gg/buoy/buglinks/frames/FrameWalker.kt +181 -0
- package/ios/BuoyFrames.m +17 -0
- package/ios/BuoyFrames.swift +640 -0
- package/ios/BuoyFramesWalk.swift +213 -0
- package/lib/commonjs/components/BugLinksBody.js +1 -0
- package/lib/commonjs/components/TeamBugs.js +1 -0
- package/lib/commonjs/components/focus.js +1 -0
- package/lib/commonjs/components/overlays.js +1 -0
- package/lib/commonjs/components/topBanners.js +1 -0
- package/lib/commonjs/engine/client.js +1 -0
- package/lib/commonjs/engine/credential.js +1 -0
- package/lib/commonjs/engine/linksStore.js +1 -0
- package/lib/commonjs/engine/loader.js +1 -0
- package/lib/commonjs/engine/opener.js +1 -0
- package/lib/commonjs/engine/otherTabs.js +1 -0
- package/lib/commonjs/engine/phoneCheck.js +1 -0
- package/lib/commonjs/engine/phoneCrypto.js +1 -0
- package/lib/commonjs/engine/randomBytes.js +1 -0
- package/lib/commonjs/engine/reporter.js +1 -0
- package/lib/commonjs/engine/sandbox.js +1 -0
- package/lib/commonjs/engine/settings.js +1 -0
- package/lib/commonjs/engine/tabStorage.js +1 -0
- package/lib/commonjs/engine/teamKeys.js +1 -0
- package/lib/commonjs/engine/teamList.js +1 -0
- package/lib/commonjs/index.js +1 -0
- package/lib/commonjs/media/BuoyMask.js +1 -0
- package/lib/commonjs/media/frames.js +1 -0
- package/lib/commonjs/media/nativeFrames.js +1 -0
- package/lib/commonjs/media/nativeFrames.web.js +1 -0
- package/lib/commonjs/media/webFrames.js +14 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +1 -0
- package/lib/commonjs/sync/bugLinksSyncAdapter.js +1 -0
- package/lib/module/components/BugLinksBody.js +1 -0
- package/lib/module/components/TeamBugs.js +1 -0
- package/lib/module/components/focus.js +1 -0
- package/lib/module/components/overlays.js +1 -0
- package/lib/module/components/topBanners.js +1 -0
- package/lib/module/engine/client.js +1 -0
- package/lib/module/engine/credential.js +1 -0
- package/lib/module/engine/linksStore.js +1 -0
- package/lib/module/engine/loader.js +1 -0
- package/lib/module/engine/opener.js +1 -0
- package/lib/module/engine/otherTabs.js +1 -0
- package/lib/module/engine/phoneCheck.js +1 -0
- package/lib/module/engine/phoneCrypto.js +1 -0
- package/lib/module/engine/randomBytes.js +1 -0
- package/lib/module/engine/reporter.js +1 -0
- package/lib/module/engine/sandbox.js +1 -0
- package/lib/module/engine/settings.js +1 -0
- package/lib/module/engine/tabStorage.js +1 -0
- package/lib/module/engine/teamKeys.js +1 -0
- package/lib/module/engine/teamList.js +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/media/BuoyMask.js +1 -0
- package/lib/module/media/frames.js +1 -0
- package/lib/module/media/nativeFrames.js +1 -0
- package/lib/module/media/nativeFrames.web.js +1 -0
- package/lib/module/media/webFrames.js +14 -0
- package/lib/module/preset.js +1 -0
- package/lib/module/sync/bugLinksSyncAdapter.js +1 -0
- package/lib/typescript/components/BugLinksBody.d.ts +2 -0
- package/lib/typescript/components/TeamBugs.d.ts +8 -0
- package/lib/typescript/components/focus.d.ts +6 -0
- package/lib/typescript/components/overlays.d.ts +10 -0
- package/lib/typescript/components/topBanners.d.ts +35 -0
- package/lib/typescript/engine/client.d.ts +78 -0
- package/lib/typescript/engine/credential.d.ts +11 -0
- package/lib/typescript/engine/linksStore.d.ts +132 -0
- package/lib/typescript/engine/loader.d.ts +172 -0
- package/lib/typescript/engine/opener.d.ts +21 -0
- package/lib/typescript/engine/otherTabs.d.ts +52 -0
- package/lib/typescript/engine/phoneCheck.d.ts +8 -0
- package/lib/typescript/engine/phoneCrypto.d.ts +16 -0
- package/lib/typescript/engine/randomBytes.d.ts +7 -0
- package/lib/typescript/engine/reporter.d.ts +53 -0
- package/lib/typescript/engine/sandbox.d.ts +83 -0
- package/lib/typescript/engine/settings.d.ts +42 -0
- package/lib/typescript/engine/tabStorage.d.ts +6 -0
- package/lib/typescript/engine/teamKeys.d.ts +37 -0
- package/lib/typescript/engine/teamList.d.ts +63 -0
- package/lib/typescript/index.d.ts +23 -0
- package/lib/typescript/media/BuoyMask.d.ts +19 -0
- package/lib/typescript/media/frames.d.ts +34 -0
- package/lib/typescript/media/nativeFrames.d.ts +2 -0
- package/lib/typescript/media/nativeFrames.web.d.ts +2 -0
- package/lib/typescript/media/webFrames.d.ts +178 -0
- package/lib/typescript/preset.d.ts +50 -0
- package/lib/typescript/sync/bugLinksSyncAdapter.d.ts +41 -0
- package/lib/web/index.mjs +20 -0
- package/package.json +119 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// What to box in a frame (research/bug-links-2026-10/media/SPEC.md, H rules).
|
|
2
|
+
// Runs on the main thread. It never throws: anything it can't finish is a
|
|
3
|
+
// reason, and the frame is dropped (H2).
|
|
4
|
+
package gg.buoy.buglinks.frames
|
|
5
|
+
|
|
6
|
+
import android.graphics.Matrix
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.os.SystemClock
|
|
9
|
+
import android.text.InputType
|
|
10
|
+
import android.text.method.PasswordTransformationMethod
|
|
11
|
+
import android.view.SurfaceView
|
|
12
|
+
import android.view.TextureView
|
|
13
|
+
import android.view.View
|
|
14
|
+
import android.view.ViewGroup
|
|
15
|
+
import android.webkit.WebView
|
|
16
|
+
import android.widget.EditText
|
|
17
|
+
import android.widget.ImageView
|
|
18
|
+
import android.widget.TextView
|
|
19
|
+
|
|
20
|
+
data class MaskSettings(var text: Boolean = true, var images: Boolean = true, var inputs: Boolean = true)
|
|
21
|
+
|
|
22
|
+
/** One root to walk: a window's root view and where it sits in the frame, in px. */
|
|
23
|
+
class WalkRoot(val view: View, val dx: Float, val dy: Float)
|
|
24
|
+
|
|
25
|
+
class Walk {
|
|
26
|
+
/** Every part to box, by view, in frame points. */
|
|
27
|
+
val parts = HashMap<View, RectF>()
|
|
28
|
+
var buoyOpen = false
|
|
29
|
+
var failed: String? = null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
enum class PartKind { NONE, TEXT, INPUT, SECURE, IMAGE, WEB, OPAQUE }
|
|
33
|
+
|
|
34
|
+
object FrameWalker {
|
|
35
|
+
val buoyOpenIds = setOf("dial-devtools-root", "buoy-tool-host")
|
|
36
|
+
const val BUOY_ROOT = "floating-devtools-root"
|
|
37
|
+
const val MASK = "buoy-mask"
|
|
38
|
+
const val UNMASK = "buoy-unmask"
|
|
39
|
+
private const val GROW = 2f // H10
|
|
40
|
+
|
|
41
|
+
private class Item(val view: View, val root: WalkRoot, val unmask: Boolean, val inBuoy: Boolean)
|
|
42
|
+
|
|
43
|
+
fun walk(roots: List<WalkRoot>, frameW: Float, frameH: Float, density: Float, settings: MaskSettings, budgetMs: Long): Walk {
|
|
44
|
+
val out = Walk()
|
|
45
|
+
val start = SystemClock.uptimeMillis()
|
|
46
|
+
val stack = ArrayList<Item>()
|
|
47
|
+
for (r in roots.asReversed()) stack.add(Item(r.view, r, unmask = false, inBuoy = false))
|
|
48
|
+
val screenArea = frameW * frameH
|
|
49
|
+
var visited = 0
|
|
50
|
+
while (stack.isNotEmpty()) {
|
|
51
|
+
val item = stack.removeAt(stack.size - 1)
|
|
52
|
+
visited++
|
|
53
|
+
if (visited and 127 == 0 && SystemClock.uptimeMillis() - start > budgetMs) {
|
|
54
|
+
out.failed = "the screen has too many views to check in time"
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
val v = item.view
|
|
58
|
+
if (v.visibility != View.VISIBLE || v.alpha <= 0f) continue
|
|
59
|
+
|
|
60
|
+
val id = nativeId(v) ?: sentryMarker(v)
|
|
61
|
+
var unmask = item.unmask
|
|
62
|
+
var inBuoy = item.inBuoy
|
|
63
|
+
if (id != null) {
|
|
64
|
+
if (id in buoyOpenIds) {
|
|
65
|
+
if (rect(v, item.root, frameW, frameH, density) != null) out.buoyOpen = true
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (id == BUOY_ROOT) inBuoy = true
|
|
69
|
+
if (id == UNMASK) unmask = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val kind = classify(v)
|
|
73
|
+
val box = when {
|
|
74
|
+
kind == PartKind.SECURE -> true // H5
|
|
75
|
+
inBuoy -> {
|
|
76
|
+
val r = rect(v, item.root, frameW, frameH, density)
|
|
77
|
+
r != null && r.width() * r.height() < screenArea * 0.6f && hasContent(v) // F5
|
|
78
|
+
}
|
|
79
|
+
id == MASK -> true // H6, H8
|
|
80
|
+
kind == PartKind.TEXT -> settings.text && !unmask
|
|
81
|
+
kind == PartKind.INPUT -> settings.inputs && !unmask
|
|
82
|
+
kind == PartKind.IMAGE -> settings.images && !unmask
|
|
83
|
+
kind == PartKind.WEB || kind == PartKind.OPAQUE -> !unmask // H5a
|
|
84
|
+
else -> false
|
|
85
|
+
}
|
|
86
|
+
if (box) rect(v, item.root, frameW, frameH, density)?.let { out.parts[v] = it }
|
|
87
|
+
// H12: only skip children a box clips. Inputs are walked for secure fields.
|
|
88
|
+
if (box && clips(v) && kind != PartKind.INPUT) continue
|
|
89
|
+
if (v is ViewGroup) {
|
|
90
|
+
for (i in v.childCount - 1 downTo 0) {
|
|
91
|
+
val c = v.getChildAt(i) ?: continue
|
|
92
|
+
stack.add(Item(c, item.root, unmask, inBuoy))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fun nativeId(v: View): String? = v.getTag(com.facebook.react.R.id.view_tag_native_id) as? String
|
|
100
|
+
|
|
101
|
+
fun testId(v: View): String? = v.getTag(com.facebook.react.R.id.react_test_id) as? String
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sentry's <Sentry.Mask> and <Sentry.Unmask> count as ours, so an app that
|
|
105
|
+
* already marked its private parts for Sentry is covered here too.
|
|
106
|
+
*/
|
|
107
|
+
private fun sentryMarker(v: View): String? = when (v.javaClass.simpleName) {
|
|
108
|
+
"RNSentryReplayMask" -> MASK
|
|
109
|
+
"RNSentryReplayUnmask" -> UNMASK
|
|
110
|
+
else -> null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun classify(v: View): PartKind {
|
|
114
|
+
if (v is WebView) return PartKind.WEB
|
|
115
|
+
if (v is TextView) {
|
|
116
|
+
if (isSecret(v)) return PartKind.SECURE
|
|
117
|
+
return if (v is EditText) PartKind.INPUT else PartKind.TEXT
|
|
118
|
+
}
|
|
119
|
+
if (v is ImageView) return if (v.drawable == null) PartKind.NONE else PartKind.IMAGE
|
|
120
|
+
if (v is SurfaceView || v is TextureView) return PartKind.OPAQUE
|
|
121
|
+
val name = v.javaClass.name
|
|
122
|
+
if (name.contains("Secure")) return PartKind.SECURE
|
|
123
|
+
if (name.contains("TextInput") || name.contains("EditText")) return PartKind.INPUT
|
|
124
|
+
if (name.contains("WebView")) return PartKind.WEB
|
|
125
|
+
if (name.contains("PdfView") || name.contains("PDFView")) return PartKind.OPAQUE
|
|
126
|
+
for (w in listOf("Compose", "Skia", "MapView", "Video", "Player", "Camera", "Preview", "GLView", "Canvas", "Chart")) {
|
|
127
|
+
if (name.contains(w)) return PartKind.OPAQUE
|
|
128
|
+
}
|
|
129
|
+
if (name.contains("Image") || name.contains("Svg") || name.contains("SVG") || name.contains("Lottie") || name.contains("Rive")) return PartKind.IMAGE
|
|
130
|
+
if (name.contains("Text") || name.contains("Paragraph") || name.contains("Label")) return PartKind.TEXT
|
|
131
|
+
return PartKind.NONE
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun isSecret(t: TextView): Boolean {
|
|
135
|
+
if (t.transformationMethod is PasswordTransformationMethod) return true
|
|
136
|
+
val type = t.inputType
|
|
137
|
+
val variation = type and InputType.TYPE_MASK_VARIATION
|
|
138
|
+
val cls = type and InputType.TYPE_MASK_CLASS
|
|
139
|
+
return (cls == InputType.TYPE_CLASS_TEXT && (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD ||
|
|
140
|
+
variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)) ||
|
|
141
|
+
(cls == InputType.TYPE_CLASS_NUMBER && variation == InputType.TYPE_NUMBER_VARIATION_PASSWORD)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private fun clips(v: View): Boolean = v !is ViewGroup || v.childCount == 0 || v.clipChildren
|
|
145
|
+
|
|
146
|
+
private fun hasContent(v: View): Boolean = v !is ViewGroup || v.childCount == 0 || v.background != null
|
|
147
|
+
|
|
148
|
+
private val corners = FloatArray(8)
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Where the view is in the frame, in points: all four corners mapped through
|
|
152
|
+
* every parent's scale, turn and scroll, then the box around them, grown (H10)
|
|
153
|
+
* and clipped to the frame (H9). null when none of it shows.
|
|
154
|
+
*/
|
|
155
|
+
fun rect(v: View, root: WalkRoot, frameW: Float, frameH: Float, density: Float): RectF? {
|
|
156
|
+
val r = RectF(0f, 0f, v.width.toFloat(), v.height.toFloat())
|
|
157
|
+
var cur: View = v
|
|
158
|
+
while (cur !== root.view) {
|
|
159
|
+
val m: Matrix = cur.matrix
|
|
160
|
+
if (!m.isIdentity) {
|
|
161
|
+
corners[0] = r.left; corners[1] = r.top; corners[2] = r.right; corners[3] = r.top
|
|
162
|
+
corners[4] = r.right; corners[5] = r.bottom; corners[6] = r.left; corners[7] = r.bottom
|
|
163
|
+
m.mapPoints(corners)
|
|
164
|
+
r.set(
|
|
165
|
+
minOf(corners[0], corners[2], corners[4], corners[6]), minOf(corners[1], corners[3], corners[5], corners[7]),
|
|
166
|
+
maxOf(corners[0], corners[2], corners[4], corners[6]), maxOf(corners[1], corners[3], corners[5], corners[7]),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
r.offset(cur.left.toFloat(), cur.top.toFloat())
|
|
170
|
+
val p = cur.parent as? View ?: return null // not attached under this root
|
|
171
|
+
r.offset(-p.scrollX.toFloat(), -p.scrollY.toFloat())
|
|
172
|
+
cur = p
|
|
173
|
+
}
|
|
174
|
+
r.offset(root.dx, root.dy)
|
|
175
|
+
val out = RectF(r.left / density - GROW, r.top / density - GROW, r.right / density + GROW, r.bottom / density + GROW)
|
|
176
|
+
if (!out.intersect(0f, 0f, frameW, frameH)) return null
|
|
177
|
+
if (out.width() <= 0f || out.height() <= 0f) return null
|
|
178
|
+
out.set(kotlin.math.floor(out.left), kotlin.math.floor(out.top), kotlin.math.ceil(out.right), kotlin.math.ceil(out.bottom))
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
}
|
package/ios/BuoyFrames.m
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Bridge for the Swift module in BuoyFrames.swift. A plain RN module, so it
|
|
2
|
+
// works on the old architecture and, through the interop layer, the new one.
|
|
3
|
+
#import <React/RCTBridgeModule.h>
|
|
4
|
+
|
|
5
|
+
@interface RCT_EXTERN_MODULE(BuoyFrames, NSObject)
|
|
6
|
+
RCT_EXTERN_METHOD(configure:(NSDictionary *)options)
|
|
7
|
+
RCT_EXTERN_METHOD(status:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
8
|
+
RCT_EXTERN_METHOD(takeMedia:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
RCT_EXTERN_METHOD(captureLast:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
RCT_EXTERN_METHOD(probe:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
RCT_EXTERN_METHOD(resume)
|
|
12
|
+
RCT_EXTERN_METHOD(startVideo:(BOOL)always)
|
|
13
|
+
RCT_EXTERN_METHOD(stopVideo)
|
|
14
|
+
RCT_EXTERN_METHOD(mark)
|
|
15
|
+
RCT_EXTERN_METHOD(clear)
|
|
16
|
+
RCT_EXTERN_METHOD(invalidate)
|
|
17
|
+
@end
|