collavre_github 0.6.0 → 0.6.2
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.
- checksums.yaml +4 -4
- data/app/javascript/__tests__/github_wizard_state.test.js +57 -0
- data/app/javascript/collavre_github.js +25 -15
- data/app/javascript/github_wizard_state.js +40 -0
- data/app/models/collavre_github/repository_link.rb +8 -0
- data/app/services/collavre_github/markdown_sync/incremental_sync_service.rb +14 -11
- data/app/services/collavre_github/markdown_sync/initial_import_service.rb +3 -3
- data/config/initializers/read_only_source.rb +13 -0
- data/db/migrate/20260711000000_encrypt_github_repository_link_webhook_secrets.rb +54 -0
- data/db/seeds.rb +1 -1
- data/lib/collavre_github/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c290ca646f38dca20fc5579da040eb6afe437b763298b130b4cfdf33528b53b
|
|
4
|
+
data.tar.gz: 42b03d3de6dc5b1e7c3150b0acbf42400d1057f0c3dc0281ede2e91f13e9e70e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5a7fd65770ca4b23ad127dacef54e615300a1fa7b695ff2e7cbb071cef667457fec581d469d893c5a7c3e41be844fe24f9f32ee4c42034738fbb46b08c41448
|
|
7
|
+
data.tar.gz: 0c011cec94b20d833d185df0eb5666309f68814700a55a94c1ec41fe1be5d6105639c0b79f6b4c577275a81c36a047dbb17c71cb3272d7559a96ec93453804f8
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { deriveConnectState, shouldShowConnectNext } from '../github_wizard_state.js';
|
|
2
|
+
|
|
3
|
+
describe('deriveConnectState', () => {
|
|
4
|
+
test('connected user with selected repos: connected + existing', () => {
|
|
5
|
+
expect(deriveConnectState({ connected: true, selected_repositories: ['a/b'] }))
|
|
6
|
+
.toEqual({ userConnected: true, hasExistingIntegration: true });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('connected user with no selected repos: connected, no existing', () => {
|
|
10
|
+
expect(deriveConnectState({ connected: true, selected_repositories: [] }))
|
|
11
|
+
.toEqual({ userConnected: true, hasExistingIntegration: false });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('connected user ignores all_repositories for existing flag', () => {
|
|
15
|
+
// A connected user's existing state comes from their own selection,
|
|
16
|
+
// never from other members' all_repositories.
|
|
17
|
+
expect(deriveConnectState({ connected: true, selected_repositories: [], all_repositories: ['x/y', 'x/z'] }))
|
|
18
|
+
.toEqual({ userConnected: true, hasExistingIntegration: false });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('unconnected user with repos linked by others: not connected but existing', () => {
|
|
22
|
+
expect(deriveConnectState({ connected: false, all_repositories: ['x/y', 'x/z', 'x/w'] }))
|
|
23
|
+
.toEqual({ userConnected: false, hasExistingIntegration: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('unconnected user with no repos: not connected, no existing', () => {
|
|
27
|
+
expect(deriveConnectState({ connected: false, all_repositories: [] }))
|
|
28
|
+
.toEqual({ userConnected: false, hasExistingIntegration: false });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('tolerates missing/empty payload', () => {
|
|
32
|
+
expect(deriveConnectState()).toEqual({ userConnected: false, hasExistingIntegration: false });
|
|
33
|
+
expect(deriveConnectState({})).toEqual({ userConnected: false, hasExistingIntegration: false });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('shouldShowConnectNext', () => {
|
|
38
|
+
test('shows Next only when the current user is connected AND repos exist', () => {
|
|
39
|
+
expect(shouldShowConnectNext({ hasExistingIntegration: true, userConnected: true })).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('hides Next for an unconnected user even when repos exist (regression)', () => {
|
|
43
|
+
// Repos linked by other members must not let an unauthenticated user
|
|
44
|
+
// advance into an empty organization list — they must log in first.
|
|
45
|
+
expect(shouldShowConnectNext({ hasExistingIntegration: true, userConnected: false })).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('hides Next when there is no existing integration', () => {
|
|
49
|
+
expect(shouldShowConnectNext({ hasExistingIntegration: false, userConnected: true })).toBe(false);
|
|
50
|
+
expect(shouldShowConnectNext({ hasExistingIntegration: false, userConnected: false })).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('tolerates missing state', () => {
|
|
54
|
+
expect(shouldShowConnectNext()).toBe(false);
|
|
55
|
+
expect(shouldShowConnectNext({})).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { csrfToken, showError, clearError, updateStepVisibility, openOAuthPopup, fetchWithCsrf, setupModalClose } from 'collavre/modules/integration_wizard';
|
|
2
|
+
import { alertDialog, confirmDialog } from 'collavre/lib/utils/dialog';
|
|
3
|
+
import { deriveConnectState, shouldShowConnectNext } from './github_wizard_state.js';
|
|
2
4
|
|
|
3
5
|
let githubIntegrationInitialized = false;
|
|
4
6
|
|
|
@@ -50,6 +52,10 @@ if (!githubIntegrationInitialized) {
|
|
|
50
52
|
let webhookDetails = {};
|
|
51
53
|
|
|
52
54
|
let hasExistingIntegration = false;
|
|
55
|
+
// Whether the *current* user has their own connected GitHub account.
|
|
56
|
+
// hasExistingIntegration can be true from other members' linked repos while
|
|
57
|
+
// this user is still unauthenticated, so the two must be tracked separately.
|
|
58
|
+
let userConnected = false;
|
|
53
59
|
let selectedReposForDeletion = new Set();
|
|
54
60
|
|
|
55
61
|
const markdownSyncList = document.getElementById('github-markdown-sync-list');
|
|
@@ -63,6 +69,7 @@ if (!githubIntegrationInitialized) {
|
|
|
63
69
|
webhookDetails = {};
|
|
64
70
|
|
|
65
71
|
hasExistingIntegration = false;
|
|
72
|
+
userConnected = false;
|
|
66
73
|
selectedReposForDeletion = new Set();
|
|
67
74
|
statusEl.textContent = '';
|
|
68
75
|
errorEl.style.display = 'none';
|
|
@@ -100,7 +107,10 @@ if (!githubIntegrationInitialized) {
|
|
|
100
107
|
|
|
101
108
|
if (currentStep === 'connect') {
|
|
102
109
|
prevBtn.style.display = 'none';
|
|
103
|
-
|
|
110
|
+
// Only surface "Next" when the current user is actually connected. When
|
|
111
|
+
// existing repos come solely from other members, the user must log in
|
|
112
|
+
// first — otherwise Next leads to an empty (0 org) dead end.
|
|
113
|
+
if (shouldShowConnectNext({ hasExistingIntegration, userConnected })) {
|
|
104
114
|
nextBtn.style.display = 'block';
|
|
105
115
|
nextBtn.disabled = false;
|
|
106
116
|
} else {
|
|
@@ -197,18 +207,19 @@ if (!githubIntegrationInitialized) {
|
|
|
197
207
|
fetch(`/github/creatives/${creativeId}/integration`, { headers: { Accept: 'application/json' } })
|
|
198
208
|
.then(function (response) { return response.json(); })
|
|
199
209
|
.then(function (data) {
|
|
210
|
+
var connectState = deriveConnectState(data);
|
|
211
|
+
userConnected = connectState.userConnected;
|
|
212
|
+
hasExistingIntegration = connectState.hasExistingIntegration;
|
|
213
|
+
|
|
200
214
|
if (!data.connected) {
|
|
201
|
-
|
|
202
|
-
if (allRepos.length > 0) {
|
|
215
|
+
if (hasExistingIntegration) {
|
|
203
216
|
// Other users have linked repositories to this creative
|
|
204
217
|
statusEl.textContent = existingMessage;
|
|
205
|
-
|
|
206
|
-
renderExistingConnections(allRepos, true);
|
|
218
|
+
renderExistingConnections(data.all_repositories || [], true);
|
|
207
219
|
if (connectMessage) connectMessage.style.display = 'none';
|
|
208
220
|
if (loginBtn) loginBtn.style.display = 'inline-block';
|
|
209
221
|
} else {
|
|
210
222
|
statusEl.textContent = '';
|
|
211
|
-
hasExistingIntegration = false;
|
|
212
223
|
renderExistingConnections([]);
|
|
213
224
|
if (connectMessage) connectMessage.style.display = '';
|
|
214
225
|
if (loginBtn) loginBtn.style.display = 'inline-block';
|
|
@@ -229,8 +240,7 @@ if (!githubIntegrationInitialized) {
|
|
|
229
240
|
}
|
|
230
241
|
});
|
|
231
242
|
|
|
232
|
-
hasExistingIntegration
|
|
233
|
-
|
|
243
|
+
// hasExistingIntegration was derived above from selected_repositories.
|
|
234
244
|
if (loginBtn) loginBtn.style.display = 'none';
|
|
235
245
|
renderExistingConnections(Array.from(selectedRepos));
|
|
236
246
|
|
|
@@ -513,7 +523,7 @@ if (!githubIntegrationInitialized) {
|
|
|
513
523
|
|
|
514
524
|
|
|
515
525
|
updateSummary();
|
|
516
|
-
|
|
526
|
+
alertDialog(modal.dataset.successMessage);
|
|
517
527
|
})
|
|
518
528
|
.catch(function () {
|
|
519
529
|
showError('연동 정보를 저장하지 못했습니다.');
|
|
@@ -523,7 +533,7 @@ if (!githubIntegrationInitialized) {
|
|
|
523
533
|
openBtn.addEventListener('click', function () {
|
|
524
534
|
creativeId = openBtn.dataset.creativeId;
|
|
525
535
|
if (!creativeId) {
|
|
526
|
-
|
|
536
|
+
alertDialog(modal.dataset.noCreative);
|
|
527
537
|
return;
|
|
528
538
|
}
|
|
529
539
|
resetWizard();
|
|
@@ -598,9 +608,9 @@ if (!githubIntegrationInitialized) {
|
|
|
598
608
|
});
|
|
599
609
|
});
|
|
600
610
|
|
|
601
|
-
deleteBtn?.addEventListener('click', function () {
|
|
611
|
+
deleteBtn?.addEventListener('click', async function () {
|
|
602
612
|
if (!creativeId) {
|
|
603
|
-
|
|
613
|
+
alertDialog(modal.dataset.noCreative);
|
|
604
614
|
return;
|
|
605
615
|
}
|
|
606
616
|
clearError();
|
|
@@ -609,7 +619,7 @@ if (!githubIntegrationInitialized) {
|
|
|
609
619
|
showError(deleteSelectWarning);
|
|
610
620
|
return;
|
|
611
621
|
}
|
|
612
|
-
if (!
|
|
622
|
+
if (!(await confirmDialog(deleteConfirm, { danger: true }))) return;
|
|
613
623
|
|
|
614
624
|
fetch(`/github/creatives/${creativeId}/integration`, {
|
|
615
625
|
method: 'DELETE',
|
|
@@ -648,7 +658,7 @@ if (!githubIntegrationInitialized) {
|
|
|
648
658
|
});
|
|
649
659
|
});
|
|
650
660
|
|
|
651
|
-
resyncBtn?.addEventListener('click', function () {
|
|
661
|
+
resyncBtn?.addEventListener('click', async function () {
|
|
652
662
|
if (!creativeId) return;
|
|
653
663
|
clearError();
|
|
654
664
|
const checkboxes = existingList ? existingList.querySelectorAll('.github-existing-repo-checkbox:checked') : [];
|
|
@@ -657,7 +667,7 @@ if (!githubIntegrationInitialized) {
|
|
|
657
667
|
showError(resyncSelectWarning);
|
|
658
668
|
return;
|
|
659
669
|
}
|
|
660
|
-
if (!
|
|
670
|
+
if (!(await confirmDialog(resyncConfirm))) return;
|
|
661
671
|
|
|
662
672
|
resyncBtn.disabled = true;
|
|
663
673
|
let completed = 0;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-logic helpers for the GitHub integration wizard's connect step.
|
|
3
|
+
* Extracted so the "can this user advance past the connect step" decision
|
|
4
|
+
* can be unit-tested without the full DOM wizard.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derive connect-step flags from the integration status payload.
|
|
9
|
+
*
|
|
10
|
+
* `userConnected` reflects whether the *current* user has their own linked
|
|
11
|
+
* GitHub account. `hasExistingIntegration` reflects whether the creative
|
|
12
|
+
* already has repositories linked — by this user when connected, or by *any*
|
|
13
|
+
* member (all_repositories) when the current user is not yet connected.
|
|
14
|
+
*
|
|
15
|
+
* @param {{connected?:boolean, all_repositories?:string[], selected_repositories?:string[]}} status
|
|
16
|
+
* @returns {{userConnected:boolean, hasExistingIntegration:boolean}}
|
|
17
|
+
*/
|
|
18
|
+
export function deriveConnectState(status) {
|
|
19
|
+
const s = status || {};
|
|
20
|
+
const userConnected = !!s.connected;
|
|
21
|
+
const repos = userConnected ? s.selected_repositories : s.all_repositories;
|
|
22
|
+
const hasExistingIntegration = (repos || []).length > 0;
|
|
23
|
+
return { userConnected, hasExistingIntegration };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the connect step's "Next" button should be shown.
|
|
28
|
+
*
|
|
29
|
+
* Existing repositories linked by *other* members must not let an
|
|
30
|
+
* unauthenticated user advance: organization lookup requires the user's own
|
|
31
|
+
* connected GitHub account, so advancing would dead-end on an empty
|
|
32
|
+
* organization list. Such a user must log in first.
|
|
33
|
+
*
|
|
34
|
+
* @param {{hasExistingIntegration?:boolean, userConnected?:boolean}} state
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function shouldShowConnectNext(state) {
|
|
38
|
+
const s = state || {};
|
|
39
|
+
return !!(s.hasExistingIntegration && s.userConnected);
|
|
40
|
+
}
|
|
@@ -6,6 +6,14 @@ module CollavreGithub
|
|
|
6
6
|
belongs_to :github_account, class_name: "CollavreGithub::Account"
|
|
7
7
|
belongs_to :markdown_root_creative, class_name: "Collavre::Creative", optional: true
|
|
8
8
|
|
|
9
|
+
# HMAC secret GitHub signs webhook deliveries with. Encrypted at rest so a
|
|
10
|
+
# DB/backup leak can't forge webhooks (mirrors CollavreGithub::Account#token
|
|
11
|
+
# and CollavreLinear::ProjectLink#webhook_secret). Non-deterministic: the
|
|
12
|
+
# value is only ever read back decrypted (WebhooksController, provisioner),
|
|
13
|
+
# never queried by ciphertext. A backfill migration re-encrypts pre-existing
|
|
14
|
+
# plaintext rows.
|
|
15
|
+
encrypts :webhook_secret, deterministic: false
|
|
16
|
+
|
|
9
17
|
validates :repository_full_name, presence: true
|
|
10
18
|
validates :webhook_secret, presence: true
|
|
11
19
|
|
|
@@ -73,7 +73,7 @@ module CollavreGithub
|
|
|
73
73
|
"sha" => @tree_cache[path]
|
|
74
74
|
)
|
|
75
75
|
source.delete("rendered_html")
|
|
76
|
-
creative.
|
|
76
|
+
creative.skip_read_only_source_validation = true
|
|
77
77
|
creative.update!(data: creative.data.merge("source" => source))
|
|
78
78
|
|
|
79
79
|
processed, blobs = processor.process(content, path)
|
|
@@ -107,7 +107,7 @@ module CollavreGithub
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
)
|
|
110
|
-
creative.
|
|
110
|
+
creative.skip_read_only_source_validation = true
|
|
111
111
|
creative.save!
|
|
112
112
|
@synced_creatives[path] = creative
|
|
113
113
|
|
|
@@ -130,15 +130,18 @@ module CollavreGithub
|
|
|
130
130
|
branch == @link.markdown_sync_branch
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
# Load all synced creatives for this repository link, indexed by path
|
|
133
|
+
# Load all synced creatives for this repository link, indexed by path.
|
|
134
|
+
#
|
|
135
|
+
# The `->`/`->>` JSON operators are portable: both PostgreSQL and modern
|
|
136
|
+
# SQLite (bundled by the `sqlite3` gem used here) support them, so a
|
|
137
|
+
# single expression works on both adapters without an
|
|
138
|
+
# `adapter_name == "PostgreSQL"` branch. Wrapping in `CAST(... AS
|
|
139
|
+
# INTEGER)` (ANSI SQL, supported by both) avoids relying on Postgres's
|
|
140
|
+
# `::integer` cast syntax, which SQLite doesn't understand.
|
|
134
141
|
def load_synced_creatives
|
|
135
|
-
scope = Collavre::Creative
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
scope = scope.where("(data->'source'->>'repository_link_id')::integer = ?", @link.id)
|
|
139
|
-
else
|
|
140
|
-
scope = scope.where("json_extract(data, '$.source.repository_link_id') = ?", @link.id)
|
|
141
|
-
end
|
|
142
|
+
scope = Collavre::Creative
|
|
143
|
+
.where(archived_at: nil)
|
|
144
|
+
.where("CAST(data -> 'source' ->> 'repository_link_id' AS INTEGER) = ?", @link.id)
|
|
142
145
|
|
|
143
146
|
scope.each_with_object({}) { |c, h| h[c.data.dig("source", "path")] = c }
|
|
144
147
|
end
|
|
@@ -216,7 +219,7 @@ module CollavreGithub
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
)
|
|
219
|
-
dir_creative.
|
|
222
|
+
dir_creative.skip_read_only_source_validation = true
|
|
220
223
|
dir_creative.save!
|
|
221
224
|
@synced_creatives[dir_path] = dir_creative
|
|
222
225
|
new_dirs << dir_creative
|
|
@@ -63,7 +63,7 @@ module CollavreGithub
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
)
|
|
66
|
-
creative.
|
|
66
|
+
creative.skip_read_only_source_validation = true
|
|
67
67
|
creative.save!
|
|
68
68
|
file_creatives << creative
|
|
69
69
|
created << creative
|
|
@@ -114,7 +114,7 @@ module CollavreGithub
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
)
|
|
117
|
-
creative.
|
|
117
|
+
creative.skip_read_only_source_validation = true
|
|
118
118
|
creative.save!
|
|
119
119
|
creative
|
|
120
120
|
end
|
|
@@ -175,7 +175,7 @@ module CollavreGithub
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
)
|
|
178
|
-
dir_creative.
|
|
178
|
+
dir_creative.skip_read_only_source_validation = true
|
|
179
179
|
dir_creative.save!
|
|
180
180
|
dir_map[current_path] = dir_creative
|
|
181
181
|
created << dir_creative
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GitHub-synced Markdown creatives are read-only in Collavre: their description
|
|
4
|
+
# is owned by the upstream repository and must not be edited in-app. Register
|
|
5
|
+
# the source type into the core read-only-source registry so core enforces the
|
|
6
|
+
# read-only behavior (validation, attachment embedding, tree write flags)
|
|
7
|
+
# without naming GitHub. Runs on boot and every reload so the registration
|
|
8
|
+
# survives Zeitwerk clearing Collavre::Creative's class state.
|
|
9
|
+
Rails.application.config.to_prepare do
|
|
10
|
+
if defined?(Collavre::Creative)
|
|
11
|
+
Collavre::Creative.register_read_only_source("github_markdown")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Re-encrypt pre-existing plaintext webhook_secret values in place now that
|
|
4
|
+
# CollavreGithub::RepositoryLink declares `encrypts :webhook_secret`. Reads the
|
|
5
|
+
# raw column with SQL (bypassing the model so we get plaintext, not a decrypt
|
|
6
|
+
# attempt), skips rows already in Active Record Encryption's JSON envelope, and
|
|
7
|
+
# writes the encrypted payload back with the model's own encryptor so the format
|
|
8
|
+
# matches runtime reads. `support_unencrypted_data = true` keeps unmigrated rows
|
|
9
|
+
# readable, so this is safe to run without downtime.
|
|
10
|
+
class EncryptGithubRepositoryLinkWebhookSecrets < ActiveRecord::Migration[8.1]
|
|
11
|
+
def up
|
|
12
|
+
say_with_time "Encrypting GitHub repository link webhook secrets" do
|
|
13
|
+
encrypt_column(CollavreGithub::RepositoryLink, :webhook_secret)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def down
|
|
18
|
+
# No-op: `encrypts` decrypts transparently, and support_unencrypted_data
|
|
19
|
+
# keeps any remaining plaintext readable.
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def encrypt_column(model, attribute)
|
|
25
|
+
table_name = model.table_name
|
|
26
|
+
connection = ActiveRecord::Base.connection
|
|
27
|
+
|
|
28
|
+
rows = connection.select_all(
|
|
29
|
+
"SELECT id, #{attribute} FROM #{table_name} WHERE #{attribute} IS NOT NULL"
|
|
30
|
+
).to_a
|
|
31
|
+
|
|
32
|
+
encryptor = model.type_for_attribute(attribute)
|
|
33
|
+
|
|
34
|
+
rows.each do |row|
|
|
35
|
+
plaintext = row[attribute.to_s]
|
|
36
|
+
next if plaintext.nil? || encrypted?(plaintext)
|
|
37
|
+
|
|
38
|
+
encrypted = encryptor.serialize(plaintext)
|
|
39
|
+
connection.execute(
|
|
40
|
+
"UPDATE #{table_name} SET #{attribute} = #{connection.quote(encrypted)} WHERE id = #{connection.quote(row['id'])}"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Active Record Encryption stores a JSON envelope containing a "p" (payload)
|
|
46
|
+
# key; treat anything already in that shape as encrypted and leave it alone.
|
|
47
|
+
def encrypted?(value)
|
|
48
|
+
return false unless value.is_a?(String)
|
|
49
|
+
|
|
50
|
+
value.start_with?("{") && value.include?('"p":')
|
|
51
|
+
rescue StandardError
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
end
|
data/db/seeds.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre_github
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -66,7 +66,9 @@ files:
|
|
|
66
66
|
- app/controllers/collavre_github/auth_controller.rb
|
|
67
67
|
- app/controllers/collavre_github/creatives/integrations_controller.rb
|
|
68
68
|
- app/controllers/collavre_github/webhooks_controller.rb
|
|
69
|
+
- app/javascript/__tests__/github_wizard_state.test.js
|
|
69
70
|
- app/javascript/collavre_github.js
|
|
71
|
+
- app/javascript/github_wizard_state.js
|
|
70
72
|
- app/jobs/collavre_github/initial_markdown_sync_job.rb
|
|
71
73
|
- app/jobs/collavre_github/markdown_sync_job.rb
|
|
72
74
|
- app/models/collavre_github/account.rb
|
|
@@ -88,6 +90,7 @@ files:
|
|
|
88
90
|
- app/views/collavre_github/auth/setup.html.erb
|
|
89
91
|
- app/views/collavre_github/integrations/_modal.html.erb
|
|
90
92
|
- config/initializers/integration_settings.rb
|
|
93
|
+
- config/initializers/read_only_source.rb
|
|
91
94
|
- config/locales/en.yml
|
|
92
95
|
- config/locales/ko.yml
|
|
93
96
|
- config/routes.rb
|
|
@@ -97,6 +100,7 @@ files:
|
|
|
97
100
|
- db/migrate/20260219095224_remove_github_gemini_prompt_from_creatives.rb
|
|
98
101
|
- db/migrate/20260412000000_add_markdown_sync_to_repository_links.rb
|
|
99
102
|
- db/migrate/20260416000000_add_markdown_root_creative_index.rb
|
|
103
|
+
- db/migrate/20260711000000_encrypt_github_repository_link_webhook_secrets.rb
|
|
100
104
|
- db/seeds.rb
|
|
101
105
|
- lib/collavre_github.rb
|
|
102
106
|
- lib/collavre_github/engine.rb
|